From a30c377409da44f4ee087f7db4e9814412e56d44 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 24 Oct 2026 02:38:39 -0400 Subject: [PATCH 01/24] Add feed-derived NZ reference ageing factors G8-r2a. Adds the projected_period_average_ratio@1 factor operation (reference_factors.py) and wires its selected factors into the shared reference-profile resolver, composition preflight and target compilation. - Operands are the authenticated Ledger JSON tokens (LexicalFloat keeps the token on a slotted float subclass; integers stay int). Factors are exact rationals with a certified root enclosure, rounded once (half-even) to six decimals and carried as p/10**6. - reference_aging.json declares the six operational factors (IRD TY2024 low/central/high 1.115056/1.117109/1.119158; P1 1.026690/1.028580/1.030467) and the 1.086069 diagnostic, each with a receipt bound to the seven-field Chronicle feed identity. Factors need re-declaration after the September QES observation (validity ends 2026-11-03). - The resolver takes factor_context; selected factors enter a version-2 selection identity, money companions take one common linear_combination weight, counts are unchanged, and non-empty interval rosters refuse until r2b. - Composition checks selected factors against the declared feed pin; compilation replays every selected factor against the loaded artifact and refuses a self-consistent forged factor or an unauthenticated source commit. reference_factors joins _COMPILE_MODULES. - S0-S5 and V3 use the central factor, V1 low, V2 high, V4 central plus the small-household residual. All 474 IRD rows stay parked on B4 measurement. Tests: 24 engine-free files, 2,832 passed and 5 skipped (UK feed not configured), one file per run. Eleven mutants killed, each by a named test. Co-Authored-By: GPT-6.1 Sol Co-Authored-By: Claude Opus 5.5 --- .../nz-reference-ageing-factors.added.md | 9 + .../src/microcosm/build/ledger_artifact.py | 48 +- .../microcosm/build/nz/country_package.json | 5 + .../microcosm/build/nz/reference_aging.json | 8386 +++++++++++++++++ .../build/nz/reference_inventory.json | 4 +- .../build/nz/reference_profiles.json | 1469 ++- .../microcosm/build/nz/target_references.json | 4743 +++++++--- .../src/microcosm/build/reference_factors.py | 711 ++ .../src/microcosm/build/reference_profiles.py | 364 +- .../src/microcosm/build/transport/compose.py | 31 +- .../build/transport/target_kernels.py | 32 +- .../shared/test_calibration_columns.py | 6 +- .../shared/test_ledger_artifact.py | 62 + .../shared/test_nz_reference_aging.py | 256 + .../shared/test_nz_spec_package.py | 72 +- .../shared/test_nz_target_references.py | 93 +- .../shared/test_reference_factors.py | 535 ++ .../shared/test_reference_profiles.py | 317 + .../test_spec_engine_country_bundles.py | 1 + .../shared/test_transport_composed_graph.py | 140 + .../shared/test_transport_target_kernels.py | 150 + .../tests/golden/nz_country_spec.json | 12 +- .../reference_factor_transport.py | 169 + .../microcosm_build/reference_factors.py | 185 + tools/generate_nz_reference_aging.py | 241 + tools/generate_nz_target_references.py | 54 +- 26 files changed, 16616 insertions(+), 1479 deletions(-) create mode 100644 changelog.d/nz-reference-ageing-factors.added.md create mode 100644 packages/microcosm-build/src/microcosm/build/nz/reference_aging.json create mode 100644 packages/microcosm-build/src/microcosm/build/reference_factors.py create mode 100644 packages/microcosm-build/tests/engine_free/shared/test_nz_reference_aging.py create mode 100644 packages/microcosm-build/tests/engine_free/shared/test_reference_factors.py create mode 100644 test_support/microcosm_build/reference_factor_transport.py create mode 100644 test_support/microcosm_build/reference_factors.py create mode 100644 tools/generate_nz_reference_aging.py diff --git a/changelog.d/nz-reference-ageing-factors.added.md b/changelog.d/nz-reference-ageing-factors.added.md new file mode 100644 index 0000000000..d625f48eb6 --- /dev/null +++ b/changelog.d/nz-reference-ageing-factors.added.md @@ -0,0 +1,9 @@ +Add declaration-driven, exact feed-derived reference ageing factors with certified +root bounds, once-only rounding, selected evidence identities and authenticated +compiler replay. Declare the reviewed NZ ageing profiles and their explicit money +and count companions while keeping the income references parked for measurement +closure. + +Keep authenticated decimal tokens in compact slotted floats that survive copying +and pickling. Bind factor evidence to reviewed feed identities while allowing +prose-only pin edits; refuse replay when the source commit cannot be authenticated. diff --git a/packages/microcosm-build/src/microcosm/build/ledger_artifact.py b/packages/microcosm-build/src/microcosm/build/ledger_artifact.py index da0eb1968b..8d35f4b2ff 100644 --- a/packages/microcosm-build/src/microcosm/build/ledger_artifact.py +++ b/packages/microcosm-build/src/microcosm/build/ledger_artifact.py @@ -64,7 +64,9 @@ "CHRONICLE_FEED_RESOURCE", "ChronicleFeedPinMismatchError", "LedgerConsumerArtifact", + "LexicalFloat", "add_ledger_artifact_args", + "chronicle_feed_identity", "load_ledger_consumer_artifact", "resolve_ledger_artifact", "validate_chronicle_feed_pin", @@ -78,6 +80,50 @@ ALLOWED_LEDGER_ASSERTIONS = frozenset(("observation", "source_projection")) DEFAULT_LEDGER_ASSERTION = "observation" CHRONICLE_FEED_RESOURCE = "chronicle_feed.json" +_FEED_IDENTITY_FIELDS = ( + "source_commit", + "facts_sha256", + "manifest_sha256", + "artifact_schema_version", + "consumer_fact_schema_versions", + "consumer_fact_schema_sha256", + "fact_row_count", +) + + +def chronicle_feed_identity(feed_pin: Mapping[str, Any]) -> dict[str, Any]: + """Select reviewed source commit and loader-verified fields, excluding prose. + + The source commit is a caller declaration unless the authenticated manifest + also carries it; the artifact loader verifies the remaining six fields. + """ + missing = sorted(set(_FEED_IDENTITY_FIELDS) - feed_pin.keys()) + if missing: + raise ValueError( + f"Chronicle factor feed identity is missing fields: {missing}." + ) + if not isinstance(feed_pin["source_commit"], str) or not feed_pin["source_commit"]: + raise ValueError("Chronicle factor feed identity requires a source commit.") + return {field: feed_pin[field] for field in _FEED_IDENTITY_FIELDS} + + +class LexicalFloat(float): + """An ordinary float retaining its authenticated JSON numeric token. + + Existing consumers and JSON serialization retain their float behavior. + Exact-arithmetic consumers can inspect ``lexical_token`` and prove that + converting the float back to decimal reproduces the published operand. + """ + + __slots__ = ("lexical_token",) + + def __new__(cls, token: str): + instance = super().__new__(cls, token) + instance.lexical_token = token + return instance + + def __reduce__(self): + return type(self), (self.lexical_token,) class ChronicleFeedPinMismatchError(ValueError): @@ -397,7 +443,7 @@ def _load_fact_rows(path: Path) -> tuple[dict[str, Any], ...]: if not stripped: continue try: - row = json.loads(stripped) + row = json.loads(stripped, parse_float=LexicalFloat) except json.JSONDecodeError as exc: raise ValueError( f"Invalid Ledger facts JSONL row {line_number}: {exc.msg}" diff --git a/packages/microcosm-build/src/microcosm/build/nz/country_package.json b/packages/microcosm-build/src/microcosm/build/nz/country_package.json index 2f1c4b8e66..7ada3b19fb 100644 --- a/packages/microcosm-build/src/microcosm/build/nz/country_package.json +++ b/packages/microcosm-build/src/microcosm/build/nz/country_package.json @@ -97,6 +97,11 @@ "kind": "legacy_json", "schema_id": "legacy_json" }, + { + "path": "reference_aging.json", + "kind": "legacy_json", + "schema_id": "legacy_json" + }, { "path": "reference_inventory.json", "kind": "legacy_json", diff --git a/packages/microcosm-build/src/microcosm/build/nz/reference_aging.json b/packages/microcosm-build/src/microcosm/build/nz/reference_aging.json new file mode 100644 index 0000000000..a82957f2a7 --- /dev/null +++ b/packages/microcosm-build/src/microcosm/build/nz/reference_aging.json @@ -0,0 +1,8386 @@ +{ + "schema_version": 1, + "feed_identity": { + "source_commit": "8f4cfd573c0d6f580d9c51cd67e8f449a42ed174", + "facts_sha256": "03f5ea0fe60e6dc93a135ab32cd5cd6ffb56388e702a4734cdb515a956e36b18", + "manifest_sha256": "1ea2d1eeb122275a9ce7f861ca3e20e21dd695772c04ce9d71114168e92fe053", + "artifact_schema_version": "policyengine_ledger.consumer_artifact.v2", + "consumer_fact_schema_versions": [ + "chronicle.consumer_fact.v4" + ], + "consumer_fact_schema_sha256": "d953306d6f01fbc64db4372d0262d3012ee590269f8a25b276b18dca0626c6a0", + "fact_row_count": 12368 + }, + "factors": [ + { + "declaration": { + "schema_version": 1, + "id": "ird_ty2024_to_ty2027_low", + "operation": "projected_period_average_ratio", + "operation_version": 1, + "purpose": "profile_input", + "feed_identity": { + "source_commit": "8f4cfd573c0d6f580d9c51cd67e8f449a42ed174", + "facts_sha256": "03f5ea0fe60e6dc93a135ab32cd5cd6ffb56388e702a4734cdb515a956e36b18", + "manifest_sha256": "1ea2d1eeb122275a9ce7f861ca3e20e21dd695772c04ce9d71114168e92fe053", + "artifact_schema_version": "policyengine_ledger.consumer_artifact.v2", + "consumer_fact_schema_versions": [ + "chronicle.consumer_fact.v4" + ], + "consumer_fact_schema_sha256": "d953306d6f01fbc64db4372d0262d3012ee590269f8a25b276b18dca0626c6a0", + "fact_row_count": 12368 + }, + "source_period": 2024, + "target_period": 2027, + "source_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2023-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2023q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:cfbce1343aa646fce0411631", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2023-04-01", + "end_date": "2023-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2023-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2023q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:f9f4f127d080fe2f06617947", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2023-07-01", + "end_date": "2023-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 1 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2023-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2023q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:fb5938810b7b37ae8534e5f7", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2023-10-01", + "end_date": "2023-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 2 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2024-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2024q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:854e91ec1dd95e396659ad8e", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2024-01-01", + "end_date": "2024-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 3 + } + ], + "target_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-04-01", + "end_date": "2026-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 0, + "projection_offset": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2026-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 1, + "projection_offset": 1 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-10-01", + "end_date": "2026-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 2, + "projection_offset": 2 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2027-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2027q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2027-01-01", + "end_date": "2027-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 3, + "projection_offset": 3 + } + ], + "growth": { + "selector": { + "source_name": "treasury", + "source_measure_id": "average_weekly_earnings_annual_percentage_change", + "source_concept": "treasury.prefu26.average_weekly_earnings_annual_percentage_change", + "entity_name": "government", + "period_type": "fiscal_year", + "period_value": 2026, + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "treasury.prefu26.average_weekly_earnings.fy2026", + "dimensions": [ + "earnings_component", + "publication_status" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "publication_status": "Forecast" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:aba7d3d54489b068116e6a25", + "assertion": "source_projection", + "source_table": "PREFU 2026 Table 4.1 - Key economic assumptions for fiscal forecasts" + }, + "validation": { + "unit": "percent", + "source_sha256": "e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9", + "source_vintage": "prefu_2026_09_29", + "entity_role": "forecaster", + "provenance_class": "model_output", + "assertion": "source_projection", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2027-06-30" + }, + "source_column_id": "year_ending_30_june_2027" + }, + "unit_multiplier": "0.01", + "scenario_delta": "-0.5", + "assertion_policy": "allow_source_projection", + "sensitivity_provenance": [ + "d1599: symmetric ±0.5 percentage-point Microcosm sensitivity; high is not a Treasury forecast path" + ] + }, + "subperiods_per_year": 4, + "projection_policy": "observed_preferred_latest_contiguous", + "rounding": { + "digits": 6, + "mode": "nearest_half_even" + }, + "expected_operational_factor": { + "numerator": 1115056, + "denominator": 1000000 + }, + "validity": { + "declared_on": "2026-10-23", + "valid_from": "2026-10-23", + "valid_until": "2026-11-03", + "as_of": "2026-10-23" + }, + "evidence": [ + "Stats NZ QES Table 7; QEMQ.SBSZ9A; ordinary-time FTE, all sectors, actual", + "Treasury PREFU 2026 Table 4.1; year ending 30 June 2027; Chronicle period 2026" + ], + "decision_references": [ + "d1599; IRD band ageing factors, 22 October 2026", + "G8-r2 hub rulings 3, 23 October 2026, sections 1–2" + ] + }, + "operational_factor": { + "numerator": 1115056, + "denominator": 1000000 + }, + "receipt": { + "schema_version": 1, + "operation": "projected_period_average_ratio", + "operation_version": 1, + "factor_id": "ird_ty2024_to_ty2027_low", + "declaration_sha256": "4325961ede06db69ec91791cc1addf49f889f277fd62791778754b28830bbcba", + "feed_identity": { + "source_commit": "8f4cfd573c0d6f580d9c51cd67e8f449a42ed174", + "facts_sha256": "03f5ea0fe60e6dc93a135ab32cd5cd6ffb56388e702a4734cdb515a956e36b18", + "manifest_sha256": "1ea2d1eeb122275a9ce7f861ca3e20e21dd695772c04ce9d71114168e92fe053", + "artifact_schema_version": "policyengine_ledger.consumer_artifact.v2", + "consumer_fact_schema_versions": [ + "chronicle.consumer_fact.v4" + ], + "consumer_fact_schema_sha256": "d953306d6f01fbc64db4372d0262d3012ee590269f8a25b276b18dca0626c6a0", + "fact_row_count": 12368 + }, + "source_period": 2024, + "target_period": 2027, + "source_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2023-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2023q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:cfbce1343aa646fce0411631", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:cfbce1343aa646fce0411631", + "decimal_operand": "1502.27", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1502.27", + "recovered_decimal": "1502.27", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2023-04-01", + "end_date": "2023-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2023-Q2" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2025-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_size_bytes": 135784, + "source_table": "Quarterly Employment Survey June 2025 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2025-quarter/Download-data/quarterly-employment-survey-june-2025-quarter.xlsx", + "vintage": "stats_nz_qes_june_2025" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2023-06-30", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Jun 2023 quarter", + "start_date": "2023-04-01" + }, + "weight": "1", + "sequence": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2023-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2023q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:f9f4f127d080fe2f06617947", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:f9f4f127d080fe2f06617947", + "decimal_operand": "1528.92", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1528.92", + "recovered_decimal": "1528.92", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2023-07-01", + "end_date": "2023-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2023-Q3" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2025-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_size_bytes": 135784, + "source_table": "Quarterly Employment Survey June 2025 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2025-quarter/Download-data/quarterly-employment-survey-june-2025-quarter.xlsx", + "vintage": "stats_nz_qes_june_2025" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2023-09-30", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Sep 2023 quarter", + "start_date": "2023-07-01" + }, + "weight": "1", + "sequence": 1 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2023-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2023q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:fb5938810b7b37ae8534e5f7", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:fb5938810b7b37ae8534e5f7", + "decimal_operand": "1556.16", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1556.16", + "recovered_decimal": "1556.16", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2023-10-01", + "end_date": "2023-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2023-Q4" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2025-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_size_bytes": 135784, + "source_table": "Quarterly Employment Survey June 2025 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2025-quarter/Download-data/quarterly-employment-survey-june-2025-quarter.xlsx", + "vintage": "stats_nz_qes_june_2025" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2023-12-31", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Dec 2023 quarter", + "start_date": "2023-10-01" + }, + "weight": "1", + "sequence": 2 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2024-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2024q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:854e91ec1dd95e396659ad8e", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:854e91ec1dd95e396659ad8e", + "decimal_operand": "1563.54", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1563.54", + "recovered_decimal": "1563.54", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2024-01-01", + "end_date": "2024-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2024-Q1" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2025-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_size_bytes": 135784, + "source_table": "Quarterly Employment Survey June 2025 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2025-quarter/Download-data/quarterly-employment-survey-june-2025-quarter.xlsx", + "vintage": "stats_nz_qes_june_2025" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2024-03-31", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Mar 2024 quarter", + "start_date": "2024-01-01" + }, + "weight": "1", + "sequence": 3 + } + ], + "target_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2", + "decimal_operand": "1700.68", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1700.68", + "recovered_decimal": "1700.68", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-04-01", + "end_date": "2026-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2026-Q2" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2026-06-30", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Jun 2026 quarter", + "start_date": "2026-04-01" + }, + "status": "observed", + "sequence": 0, + "weight": "1", + "projection_offset": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2026-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "status": "projected", + "sequence": 1, + "weight": "1", + "projection_offset": 1, + "projection_anchor": { + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2" + } + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-10-01", + "end_date": "2026-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "status": "projected", + "sequence": 2, + "weight": "1", + "projection_offset": 2, + "projection_anchor": { + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2" + } + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2027-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2027q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2027-01-01", + "end_date": "2027-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "status": "projected", + "sequence": 3, + "weight": "1", + "projection_offset": 3, + "projection_anchor": { + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2" + } + } + ], + "subperiods_per_year": 4, + "projection_policy": "observed_preferred_latest_contiguous", + "denominator_mean": { + "numerator": 615089, + "denominator": 400 + }, + "growth": { + "selector": { + "source_name": "treasury", + "source_measure_id": "average_weekly_earnings_annual_percentage_change", + "source_concept": "treasury.prefu26.average_weekly_earnings_annual_percentage_change", + "entity_name": "government", + "period_type": "fiscal_year", + "period_value": 2026, + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "treasury.prefu26.average_weekly_earnings.fy2026", + "dimensions": [ + "earnings_component", + "publication_status" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "publication_status": "Forecast" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:aba7d3d54489b068116e6a25", + "assertion": "source_projection", + "source_table": "PREFU 2026 Table 4.1 - Key economic assumptions for fiscal forecasts" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:aba7d3d54489b068116e6a25", + "decimal_operand": "2.7", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "2.7", + "recovered_decimal": "2.7", + "decimal_values_equal": true + }, + "validation": { + "unit": "percent", + "source_sha256": "e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9", + "source_vintage": "prefu_2026_09_29", + "entity_role": "forecaster", + "provenance_class": "model_output", + "assertion": "source_projection", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2027-06-30" + }, + "source_column_id": "year_ending_30_june_2027" + }, + "period": { + "type": "fiscal_year", + "value": 2026 + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/treasury/treasury-prefu26-economic-assumptions/2026/e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9/prefu26-charts-data.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/treasury/treasury-prefu26-economic-assumptions/2026/e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9/prefu26-charts-data.xlsx", + "source_file": "prefu26-charts-data.xlsx", + "source_name": "treasury", + "source_sha256": "e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9", + "source_size_bytes": 664670, + "source_table": "PREFU 2026 Table 4.1 - Key economic assumptions for fiscal forecasts", + "url": "https://www.treasury.govt.nz/sites/default/files/2026-09/prefu26-charts-data.xlsx", + "vintage": "prefu_2026_09_29" + }, + "unit": "percent", + "assertion": "source_projection", + "provenance_class": "model_output", + "entity_role": "forecaster", + "period_coverage": { + "basis": "fiscal", + "end_date": "2027-06-30", + "notes": "Publisher Year ending 30 June 2027; Chronicle opening-year fiscal_year:2026. Publisher column status is Forecast. Footnote 3 (Table 4.1!C21:D21): Ordinary time. Footnote 4 (C22:D22): Measures are actuals except for real GDP and nominal GDP. 2026 earnings are therefore actuals despite the Forecast4 column heading; only 2027–2031 earnings are forecasts. The published annual percentage change is retained unchanged; no annual average, level, factor or period alignment is calculated.", + "source_period_label": "Year ending 30 June 2027", + "start_date": "2026-07-01" + }, + "unit_multiplier": "0.01", + "scenario_delta": "-0.5", + "sensitivity_provenance": [ + "d1599: symmetric ±0.5 percentage-point Microcosm sensitivity; high is not a Treasury forecast path" + ], + "effective_growth": { + "numerator": 11, + "denominator": 500 + } + }, + "root_bounds": { + "lower": { + "numerator": 50272759932393, + "denominator": 50000000000000 + }, + "upper": { + "numerator": 100545519864787, + "denominator": 100000000000000 + }, + "exact": false, + "decimal_places": 14, + "degree": 4, + "radicand": { + "numerator": 511, + "denominator": 500 + } + }, + "numerator_mean_bounds": { + "lower": { + "numerator": 21433087509261019469100089110351817978329164269, + "denominator": 12500000000000000000000000000000000000000000 + }, + "upper": { + "numerator": 171464700074090725365853333139754513013059781351, + "denominator": 100000000000000000000000000000000000000000000 + } + }, + "unrounded_factor_bounds": { + "lower": { + "numerator": 21433087509261019469100089110351817978329164269, + "denominator": 19221531250000000000000000000000000000000000000 + }, + "upper": { + "numerator": 171464700074090725365853333139754513013059781351, + "denominator": 153772250000000000000000000000000000000000000000 + } + }, + "rounding": { + "digits": 6, + "mode": "nearest_half_even" + }, + "operational_factor": { + "numerator": 1115056, + "denominator": 1000000 + }, + "validity": { + "declared_on": "2026-10-23", + "valid_from": "2026-10-23", + "valid_until": "2026-11-03", + "as_of": "2026-10-23" + }, + "evidence": [ + "Stats NZ QES Table 7; QEMQ.SBSZ9A; ordinary-time FTE, all sectors, actual", + "Treasury PREFU 2026 Table 4.1; year ending 30 June 2027; Chronicle period 2026" + ], + "decision_references": [ + "d1599; IRD band ageing factors, 22 October 2026", + "G8-r2 hub rulings 3, 23 October 2026, sections 1–2" + ], + "receipt_sha256": "beb087aeff370b0de497abb10ea2558533ca9a9995b556dc1d63d70327ad3885" + } + }, + { + "declaration": { + "schema_version": 1, + "id": "ird_ty2024_to_ty2027_central", + "operation": "projected_period_average_ratio", + "operation_version": 1, + "purpose": "profile_input", + "feed_identity": { + "source_commit": "8f4cfd573c0d6f580d9c51cd67e8f449a42ed174", + "facts_sha256": "03f5ea0fe60e6dc93a135ab32cd5cd6ffb56388e702a4734cdb515a956e36b18", + "manifest_sha256": "1ea2d1eeb122275a9ce7f861ca3e20e21dd695772c04ce9d71114168e92fe053", + "artifact_schema_version": "policyengine_ledger.consumer_artifact.v2", + "consumer_fact_schema_versions": [ + "chronicle.consumer_fact.v4" + ], + "consumer_fact_schema_sha256": "d953306d6f01fbc64db4372d0262d3012ee590269f8a25b276b18dca0626c6a0", + "fact_row_count": 12368 + }, + "source_period": 2024, + "target_period": 2027, + "source_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2023-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2023q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:cfbce1343aa646fce0411631", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2023-04-01", + "end_date": "2023-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2023-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2023q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:f9f4f127d080fe2f06617947", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2023-07-01", + "end_date": "2023-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 1 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2023-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2023q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:fb5938810b7b37ae8534e5f7", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2023-10-01", + "end_date": "2023-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 2 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2024-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2024q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:854e91ec1dd95e396659ad8e", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2024-01-01", + "end_date": "2024-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 3 + } + ], + "target_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-04-01", + "end_date": "2026-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 0, + "projection_offset": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2026-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 1, + "projection_offset": 1 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-10-01", + "end_date": "2026-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 2, + "projection_offset": 2 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2027-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2027q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2027-01-01", + "end_date": "2027-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 3, + "projection_offset": 3 + } + ], + "growth": { + "selector": { + "source_name": "treasury", + "source_measure_id": "average_weekly_earnings_annual_percentage_change", + "source_concept": "treasury.prefu26.average_weekly_earnings_annual_percentage_change", + "entity_name": "government", + "period_type": "fiscal_year", + "period_value": 2026, + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "treasury.prefu26.average_weekly_earnings.fy2026", + "dimensions": [ + "earnings_component", + "publication_status" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "publication_status": "Forecast" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:aba7d3d54489b068116e6a25", + "assertion": "source_projection", + "source_table": "PREFU 2026 Table 4.1 - Key economic assumptions for fiscal forecasts" + }, + "validation": { + "unit": "percent", + "source_sha256": "e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9", + "source_vintage": "prefu_2026_09_29", + "entity_role": "forecaster", + "provenance_class": "model_output", + "assertion": "source_projection", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2027-06-30" + }, + "source_column_id": "year_ending_30_june_2027" + }, + "unit_multiplier": "0.01", + "scenario_delta": "0", + "assertion_policy": "allow_source_projection", + "sensitivity_provenance": [ + "d1599: symmetric ±0.5 percentage-point Microcosm sensitivity; high is not a Treasury forecast path" + ] + }, + "subperiods_per_year": 4, + "projection_policy": "observed_preferred_latest_contiguous", + "rounding": { + "digits": 6, + "mode": "nearest_half_even" + }, + "expected_operational_factor": { + "numerator": 1117109, + "denominator": 1000000 + }, + "validity": { + "declared_on": "2026-10-23", + "valid_from": "2026-10-23", + "valid_until": "2026-11-03", + "as_of": "2026-10-23" + }, + "evidence": [ + "Stats NZ QES Table 7; QEMQ.SBSZ9A; ordinary-time FTE, all sectors, actual", + "Treasury PREFU 2026 Table 4.1; year ending 30 June 2027; Chronicle period 2026" + ], + "decision_references": [ + "d1599; IRD band ageing factors, 22 October 2026", + "G8-r2 hub rulings 3, 23 October 2026, sections 1–2" + ] + }, + "operational_factor": { + "numerator": 1117109, + "denominator": 1000000 + }, + "receipt": { + "schema_version": 1, + "operation": "projected_period_average_ratio", + "operation_version": 1, + "factor_id": "ird_ty2024_to_ty2027_central", + "declaration_sha256": "4178ab3ea62899cad556170195b74dbb41faf7ff7cde57e298d1e62447b1b004", + "feed_identity": { + "source_commit": "8f4cfd573c0d6f580d9c51cd67e8f449a42ed174", + "facts_sha256": "03f5ea0fe60e6dc93a135ab32cd5cd6ffb56388e702a4734cdb515a956e36b18", + "manifest_sha256": "1ea2d1eeb122275a9ce7f861ca3e20e21dd695772c04ce9d71114168e92fe053", + "artifact_schema_version": "policyengine_ledger.consumer_artifact.v2", + "consumer_fact_schema_versions": [ + "chronicle.consumer_fact.v4" + ], + "consumer_fact_schema_sha256": "d953306d6f01fbc64db4372d0262d3012ee590269f8a25b276b18dca0626c6a0", + "fact_row_count": 12368 + }, + "source_period": 2024, + "target_period": 2027, + "source_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2023-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2023q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:cfbce1343aa646fce0411631", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:cfbce1343aa646fce0411631", + "decimal_operand": "1502.27", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1502.27", + "recovered_decimal": "1502.27", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2023-04-01", + "end_date": "2023-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2023-Q2" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2025-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_size_bytes": 135784, + "source_table": "Quarterly Employment Survey June 2025 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2025-quarter/Download-data/quarterly-employment-survey-june-2025-quarter.xlsx", + "vintage": "stats_nz_qes_june_2025" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2023-06-30", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Jun 2023 quarter", + "start_date": "2023-04-01" + }, + "weight": "1", + "sequence": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2023-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2023q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:f9f4f127d080fe2f06617947", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:f9f4f127d080fe2f06617947", + "decimal_operand": "1528.92", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1528.92", + "recovered_decimal": "1528.92", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2023-07-01", + "end_date": "2023-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2023-Q3" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2025-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_size_bytes": 135784, + "source_table": "Quarterly Employment Survey June 2025 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2025-quarter/Download-data/quarterly-employment-survey-june-2025-quarter.xlsx", + "vintage": "stats_nz_qes_june_2025" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2023-09-30", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Sep 2023 quarter", + "start_date": "2023-07-01" + }, + "weight": "1", + "sequence": 1 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2023-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2023q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:fb5938810b7b37ae8534e5f7", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:fb5938810b7b37ae8534e5f7", + "decimal_operand": "1556.16", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1556.16", + "recovered_decimal": "1556.16", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2023-10-01", + "end_date": "2023-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2023-Q4" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2025-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_size_bytes": 135784, + "source_table": "Quarterly Employment Survey June 2025 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2025-quarter/Download-data/quarterly-employment-survey-june-2025-quarter.xlsx", + "vintage": "stats_nz_qes_june_2025" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2023-12-31", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Dec 2023 quarter", + "start_date": "2023-10-01" + }, + "weight": "1", + "sequence": 2 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2024-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2024q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:854e91ec1dd95e396659ad8e", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:854e91ec1dd95e396659ad8e", + "decimal_operand": "1563.54", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1563.54", + "recovered_decimal": "1563.54", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2024-01-01", + "end_date": "2024-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2024-Q1" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2025-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_size_bytes": 135784, + "source_table": "Quarterly Employment Survey June 2025 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2025-quarter/Download-data/quarterly-employment-survey-june-2025-quarter.xlsx", + "vintage": "stats_nz_qes_june_2025" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2024-03-31", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Mar 2024 quarter", + "start_date": "2024-01-01" + }, + "weight": "1", + "sequence": 3 + } + ], + "target_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2", + "decimal_operand": "1700.68", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1700.68", + "recovered_decimal": "1700.68", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-04-01", + "end_date": "2026-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2026-Q2" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2026-06-30", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Jun 2026 quarter", + "start_date": "2026-04-01" + }, + "status": "observed", + "sequence": 0, + "weight": "1", + "projection_offset": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2026-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "status": "projected", + "sequence": 1, + "weight": "1", + "projection_offset": 1, + "projection_anchor": { + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2" + } + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-10-01", + "end_date": "2026-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "status": "projected", + "sequence": 2, + "weight": "1", + "projection_offset": 2, + "projection_anchor": { + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2" + } + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2027-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2027q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2027-01-01", + "end_date": "2027-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "status": "projected", + "sequence": 3, + "weight": "1", + "projection_offset": 3, + "projection_anchor": { + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2" + } + } + ], + "subperiods_per_year": 4, + "projection_policy": "observed_preferred_latest_contiguous", + "denominator_mean": { + "numerator": 615089, + "denominator": 400 + }, + "growth": { + "selector": { + "source_name": "treasury", + "source_measure_id": "average_weekly_earnings_annual_percentage_change", + "source_concept": "treasury.prefu26.average_weekly_earnings_annual_percentage_change", + "entity_name": "government", + "period_type": "fiscal_year", + "period_value": 2026, + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "treasury.prefu26.average_weekly_earnings.fy2026", + "dimensions": [ + "earnings_component", + "publication_status" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "publication_status": "Forecast" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:aba7d3d54489b068116e6a25", + "assertion": "source_projection", + "source_table": "PREFU 2026 Table 4.1 - Key economic assumptions for fiscal forecasts" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:aba7d3d54489b068116e6a25", + "decimal_operand": "2.7", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "2.7", + "recovered_decimal": "2.7", + "decimal_values_equal": true + }, + "validation": { + "unit": "percent", + "source_sha256": "e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9", + "source_vintage": "prefu_2026_09_29", + "entity_role": "forecaster", + "provenance_class": "model_output", + "assertion": "source_projection", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2027-06-30" + }, + "source_column_id": "year_ending_30_june_2027" + }, + "period": { + "type": "fiscal_year", + "value": 2026 + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/treasury/treasury-prefu26-economic-assumptions/2026/e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9/prefu26-charts-data.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/treasury/treasury-prefu26-economic-assumptions/2026/e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9/prefu26-charts-data.xlsx", + "source_file": "prefu26-charts-data.xlsx", + "source_name": "treasury", + "source_sha256": "e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9", + "source_size_bytes": 664670, + "source_table": "PREFU 2026 Table 4.1 - Key economic assumptions for fiscal forecasts", + "url": "https://www.treasury.govt.nz/sites/default/files/2026-09/prefu26-charts-data.xlsx", + "vintage": "prefu_2026_09_29" + }, + "unit": "percent", + "assertion": "source_projection", + "provenance_class": "model_output", + "entity_role": "forecaster", + "period_coverage": { + "basis": "fiscal", + "end_date": "2027-06-30", + "notes": "Publisher Year ending 30 June 2027; Chronicle opening-year fiscal_year:2026. Publisher column status is Forecast. Footnote 3 (Table 4.1!C21:D21): Ordinary time. Footnote 4 (C22:D22): Measures are actuals except for real GDP and nominal GDP. 2026 earnings are therefore actuals despite the Forecast4 column heading; only 2027–2031 earnings are forecasts. The published annual percentage change is retained unchanged; no annual average, level, factor or period alignment is calculated.", + "source_period_label": "Year ending 30 June 2027", + "start_date": "2026-07-01" + }, + "unit_multiplier": "0.01", + "scenario_delta": "0", + "sensitivity_provenance": [ + "d1599: symmetric ±0.5 percentage-point Microcosm sensitivity; high is not a Treasury forecast path" + ], + "effective_growth": { + "numerator": 27, + "denominator": 1000 + } + }, + "root_bounds": { + "lower": { + "numerator": 100668271307927, + "denominator": 100000000000000 + }, + "upper": { + "numerator": 12583533913491, + "denominator": 12500000000000 + }, + "exact": false, + "decimal_places": 14, + "degree": 4, + "radicand": { + "numerator": 1027, + "denominator": 1000 + } + }, + "numerator_mean_bounds": { + "lower": { + "numerator": 171780381168748188591006079321988113950935096211, + "denominator": 100000000000000000000000000000000000000000000 + }, + "upper": { + "numerator": 335508556970216332809143747140199880884173607, + "denominator": 195312500000000000000000000000000000000000 + } + }, + "unrounded_factor_bounds": { + "lower": { + "numerator": 171780381168748188591006079321988113950935096211, + "denominator": 153772250000000000000000000000000000000000000000 + }, + "upper": { + "numerator": 335508556970216332809143747140199880884173607, + "denominator": 300336425781250000000000000000000000000000000 + } + }, + "rounding": { + "digits": 6, + "mode": "nearest_half_even" + }, + "operational_factor": { + "numerator": 1117109, + "denominator": 1000000 + }, + "validity": { + "declared_on": "2026-10-23", + "valid_from": "2026-10-23", + "valid_until": "2026-11-03", + "as_of": "2026-10-23" + }, + "evidence": [ + "Stats NZ QES Table 7; QEMQ.SBSZ9A; ordinary-time FTE, all sectors, actual", + "Treasury PREFU 2026 Table 4.1; year ending 30 June 2027; Chronicle period 2026" + ], + "decision_references": [ + "d1599; IRD band ageing factors, 22 October 2026", + "G8-r2 hub rulings 3, 23 October 2026, sections 1–2" + ], + "receipt_sha256": "6234283ef363388578f13d7833faf0c6eb0ea1743ecc66333d017e59c554a798" + } + }, + { + "declaration": { + "schema_version": 1, + "id": "ird_ty2024_to_ty2027_high", + "operation": "projected_period_average_ratio", + "operation_version": 1, + "purpose": "profile_input", + "feed_identity": { + "source_commit": "8f4cfd573c0d6f580d9c51cd67e8f449a42ed174", + "facts_sha256": "03f5ea0fe60e6dc93a135ab32cd5cd6ffb56388e702a4734cdb515a956e36b18", + "manifest_sha256": "1ea2d1eeb122275a9ce7f861ca3e20e21dd695772c04ce9d71114168e92fe053", + "artifact_schema_version": "policyengine_ledger.consumer_artifact.v2", + "consumer_fact_schema_versions": [ + "chronicle.consumer_fact.v4" + ], + "consumer_fact_schema_sha256": "d953306d6f01fbc64db4372d0262d3012ee590269f8a25b276b18dca0626c6a0", + "fact_row_count": 12368 + }, + "source_period": 2024, + "target_period": 2027, + "source_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2023-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2023q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:cfbce1343aa646fce0411631", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2023-04-01", + "end_date": "2023-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2023-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2023q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:f9f4f127d080fe2f06617947", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2023-07-01", + "end_date": "2023-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 1 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2023-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2023q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:fb5938810b7b37ae8534e5f7", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2023-10-01", + "end_date": "2023-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 2 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2024-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2024q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:854e91ec1dd95e396659ad8e", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2024-01-01", + "end_date": "2024-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 3 + } + ], + "target_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-04-01", + "end_date": "2026-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 0, + "projection_offset": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2026-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 1, + "projection_offset": 1 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-10-01", + "end_date": "2026-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 2, + "projection_offset": 2 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2027-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2027q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2027-01-01", + "end_date": "2027-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 3, + "projection_offset": 3 + } + ], + "growth": { + "selector": { + "source_name": "treasury", + "source_measure_id": "average_weekly_earnings_annual_percentage_change", + "source_concept": "treasury.prefu26.average_weekly_earnings_annual_percentage_change", + "entity_name": "government", + "period_type": "fiscal_year", + "period_value": 2026, + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "treasury.prefu26.average_weekly_earnings.fy2026", + "dimensions": [ + "earnings_component", + "publication_status" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "publication_status": "Forecast" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:aba7d3d54489b068116e6a25", + "assertion": "source_projection", + "source_table": "PREFU 2026 Table 4.1 - Key economic assumptions for fiscal forecasts" + }, + "validation": { + "unit": "percent", + "source_sha256": "e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9", + "source_vintage": "prefu_2026_09_29", + "entity_role": "forecaster", + "provenance_class": "model_output", + "assertion": "source_projection", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2027-06-30" + }, + "source_column_id": "year_ending_30_june_2027" + }, + "unit_multiplier": "0.01", + "scenario_delta": "0.5", + "assertion_policy": "allow_source_projection", + "sensitivity_provenance": [ + "d1599: symmetric ±0.5 percentage-point Microcosm sensitivity; high is not a Treasury forecast path" + ] + }, + "subperiods_per_year": 4, + "projection_policy": "observed_preferred_latest_contiguous", + "rounding": { + "digits": 6, + "mode": "nearest_half_even" + }, + "expected_operational_factor": { + "numerator": 1119158, + "denominator": 1000000 + }, + "validity": { + "declared_on": "2026-10-23", + "valid_from": "2026-10-23", + "valid_until": "2026-11-03", + "as_of": "2026-10-23" + }, + "evidence": [ + "Stats NZ QES Table 7; QEMQ.SBSZ9A; ordinary-time FTE, all sectors, actual", + "Treasury PREFU 2026 Table 4.1; year ending 30 June 2027; Chronicle period 2026" + ], + "decision_references": [ + "d1599; IRD band ageing factors, 22 October 2026", + "G8-r2 hub rulings 3, 23 October 2026, sections 1–2" + ] + }, + "operational_factor": { + "numerator": 1119158, + "denominator": 1000000 + }, + "receipt": { + "schema_version": 1, + "operation": "projected_period_average_ratio", + "operation_version": 1, + "factor_id": "ird_ty2024_to_ty2027_high", + "declaration_sha256": "29b91c04a391e207e627f99de7e7bdeb90fc0c5d0c1558bccc4a76c3731e801d", + "feed_identity": { + "source_commit": "8f4cfd573c0d6f580d9c51cd67e8f449a42ed174", + "facts_sha256": "03f5ea0fe60e6dc93a135ab32cd5cd6ffb56388e702a4734cdb515a956e36b18", + "manifest_sha256": "1ea2d1eeb122275a9ce7f861ca3e20e21dd695772c04ce9d71114168e92fe053", + "artifact_schema_version": "policyengine_ledger.consumer_artifact.v2", + "consumer_fact_schema_versions": [ + "chronicle.consumer_fact.v4" + ], + "consumer_fact_schema_sha256": "d953306d6f01fbc64db4372d0262d3012ee590269f8a25b276b18dca0626c6a0", + "fact_row_count": 12368 + }, + "source_period": 2024, + "target_period": 2027, + "source_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2023-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2023q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:cfbce1343aa646fce0411631", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:cfbce1343aa646fce0411631", + "decimal_operand": "1502.27", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1502.27", + "recovered_decimal": "1502.27", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2023-04-01", + "end_date": "2023-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2023-Q2" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2025-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_size_bytes": 135784, + "source_table": "Quarterly Employment Survey June 2025 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2025-quarter/Download-data/quarterly-employment-survey-june-2025-quarter.xlsx", + "vintage": "stats_nz_qes_june_2025" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2023-06-30", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Jun 2023 quarter", + "start_date": "2023-04-01" + }, + "weight": "1", + "sequence": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2023-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2023q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:f9f4f127d080fe2f06617947", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:f9f4f127d080fe2f06617947", + "decimal_operand": "1528.92", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1528.92", + "recovered_decimal": "1528.92", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2023-07-01", + "end_date": "2023-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2023-Q3" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2025-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_size_bytes": 135784, + "source_table": "Quarterly Employment Survey June 2025 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2025-quarter/Download-data/quarterly-employment-survey-june-2025-quarter.xlsx", + "vintage": "stats_nz_qes_june_2025" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2023-09-30", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Sep 2023 quarter", + "start_date": "2023-07-01" + }, + "weight": "1", + "sequence": 1 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2023-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2023q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:fb5938810b7b37ae8534e5f7", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:fb5938810b7b37ae8534e5f7", + "decimal_operand": "1556.16", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1556.16", + "recovered_decimal": "1556.16", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2023-10-01", + "end_date": "2023-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2023-Q4" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2025-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_size_bytes": 135784, + "source_table": "Quarterly Employment Survey June 2025 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2025-quarter/Download-data/quarterly-employment-survey-june-2025-quarter.xlsx", + "vintage": "stats_nz_qes_june_2025" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2023-12-31", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Dec 2023 quarter", + "start_date": "2023-10-01" + }, + "weight": "1", + "sequence": 2 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2024-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2024q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:854e91ec1dd95e396659ad8e", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:854e91ec1dd95e396659ad8e", + "decimal_operand": "1563.54", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1563.54", + "recovered_decimal": "1563.54", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2024-01-01", + "end_date": "2024-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2024-Q1" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2025-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_size_bytes": 135784, + "source_table": "Quarterly Employment Survey June 2025 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2025-quarter/Download-data/quarterly-employment-survey-june-2025-quarter.xlsx", + "vintage": "stats_nz_qes_june_2025" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2024-03-31", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Mar 2024 quarter", + "start_date": "2024-01-01" + }, + "weight": "1", + "sequence": 3 + } + ], + "target_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2", + "decimal_operand": "1700.68", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1700.68", + "recovered_decimal": "1700.68", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-04-01", + "end_date": "2026-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2026-Q2" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2026-06-30", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Jun 2026 quarter", + "start_date": "2026-04-01" + }, + "status": "observed", + "sequence": 0, + "weight": "1", + "projection_offset": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2026-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "status": "projected", + "sequence": 1, + "weight": "1", + "projection_offset": 1, + "projection_anchor": { + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2" + } + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-10-01", + "end_date": "2026-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "status": "projected", + "sequence": 2, + "weight": "1", + "projection_offset": 2, + "projection_anchor": { + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2" + } + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2027-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2027q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2027-01-01", + "end_date": "2027-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "status": "projected", + "sequence": 3, + "weight": "1", + "projection_offset": 3, + "projection_anchor": { + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2" + } + } + ], + "subperiods_per_year": 4, + "projection_policy": "observed_preferred_latest_contiguous", + "denominator_mean": { + "numerator": 615089, + "denominator": 400 + }, + "growth": { + "selector": { + "source_name": "treasury", + "source_measure_id": "average_weekly_earnings_annual_percentage_change", + "source_concept": "treasury.prefu26.average_weekly_earnings_annual_percentage_change", + "entity_name": "government", + "period_type": "fiscal_year", + "period_value": 2026, + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "treasury.prefu26.average_weekly_earnings.fy2026", + "dimensions": [ + "earnings_component", + "publication_status" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "publication_status": "Forecast" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:aba7d3d54489b068116e6a25", + "assertion": "source_projection", + "source_table": "PREFU 2026 Table 4.1 - Key economic assumptions for fiscal forecasts" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:aba7d3d54489b068116e6a25", + "decimal_operand": "2.7", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "2.7", + "recovered_decimal": "2.7", + "decimal_values_equal": true + }, + "validation": { + "unit": "percent", + "source_sha256": "e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9", + "source_vintage": "prefu_2026_09_29", + "entity_role": "forecaster", + "provenance_class": "model_output", + "assertion": "source_projection", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2027-06-30" + }, + "source_column_id": "year_ending_30_june_2027" + }, + "period": { + "type": "fiscal_year", + "value": 2026 + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/treasury/treasury-prefu26-economic-assumptions/2026/e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9/prefu26-charts-data.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/treasury/treasury-prefu26-economic-assumptions/2026/e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9/prefu26-charts-data.xlsx", + "source_file": "prefu26-charts-data.xlsx", + "source_name": "treasury", + "source_sha256": "e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9", + "source_size_bytes": 664670, + "source_table": "PREFU 2026 Table 4.1 - Key economic assumptions for fiscal forecasts", + "url": "https://www.treasury.govt.nz/sites/default/files/2026-09/prefu26-charts-data.xlsx", + "vintage": "prefu_2026_09_29" + }, + "unit": "percent", + "assertion": "source_projection", + "provenance_class": "model_output", + "entity_role": "forecaster", + "period_coverage": { + "basis": "fiscal", + "end_date": "2027-06-30", + "notes": "Publisher Year ending 30 June 2027; Chronicle opening-year fiscal_year:2026. Publisher column status is Forecast. Footnote 3 (Table 4.1!C21:D21): Ordinary time. Footnote 4 (C22:D22): Measures are actuals except for real GDP and nominal GDP. 2026 earnings are therefore actuals despite the Forecast4 column heading; only 2027–2031 earnings are forecasts. The published annual percentage change is retained unchanged; no annual average, level, factor or period alignment is calculated.", + "source_period_label": "Year ending 30 June 2027", + "start_date": "2026-07-01" + }, + "unit_multiplier": "0.01", + "scenario_delta": "0.5", + "sensitivity_provenance": [ + "d1599: symmetric ±0.5 percentage-point Microcosm sensitivity; high is not a Treasury forecast path" + ], + "effective_growth": { + "numerator": 4, + "denominator": 125 + } + }, + "root_bounds": { + "lower": { + "numerator": 100790575349881, + "denominator": 100000000000000 + }, + "upper": { + "numerator": 50395287674941, + "denominator": 50000000000000 + }, + "exact": false, + "decimal_places": 14, + "degree": 4, + "radicand": { + "numerator": 129, + "denominator": 125 + } + }, + "numerator_mean_bounds": { + "lower": { + "numerator": 172095423947015521933465483088234150187075944797, + "denominator": 100000000000000000000000000000000000000000000 + }, + "upper": { + "numerator": 21511927993377262490437471966978882670435826057, + "denominator": 12500000000000000000000000000000000000000000 + } + }, + "unrounded_factor_bounds": { + "lower": { + "numerator": 172095423947015521933465483088234150187075944797, + "denominator": 153772250000000000000000000000000000000000000000 + }, + "upper": { + "numerator": 21511927993377262490437471966978882670435826057, + "denominator": 19221531250000000000000000000000000000000000000 + } + }, + "rounding": { + "digits": 6, + "mode": "nearest_half_even" + }, + "operational_factor": { + "numerator": 1119158, + "denominator": 1000000 + }, + "validity": { + "declared_on": "2026-10-23", + "valid_from": "2026-10-23", + "valid_until": "2026-11-03", + "as_of": "2026-10-23" + }, + "evidence": [ + "Stats NZ QES Table 7; QEMQ.SBSZ9A; ordinary-time FTE, all sectors, actual", + "Treasury PREFU 2026 Table 4.1; year ending 30 June 2027; Chronicle period 2026" + ], + "decision_references": [ + "d1599; IRD band ageing factors, 22 October 2026", + "G8-r2 hub rulings 3, 23 October 2026, sections 1–2" + ], + "receipt_sha256": "cc047908112d30352a300efd495c5ace4a697e93d7f21dcd4f53a7cdb175db3f" + } + }, + { + "declaration": { + "schema_version": 1, + "id": "p1_ty2026_to_ty2027_low", + "operation": "projected_period_average_ratio", + "operation_version": 1, + "purpose": "profile_input", + "feed_identity": { + "source_commit": "8f4cfd573c0d6f580d9c51cd67e8f449a42ed174", + "facts_sha256": "03f5ea0fe60e6dc93a135ab32cd5cd6ffb56388e702a4734cdb515a956e36b18", + "manifest_sha256": "1ea2d1eeb122275a9ce7f861ca3e20e21dd695772c04ce9d71114168e92fe053", + "artifact_schema_version": "policyengine_ledger.consumer_artifact.v2", + "consumer_fact_schema_versions": [ + "chronicle.consumer_fact.v4" + ], + "consumer_fact_schema_sha256": "d953306d6f01fbc64db4372d0262d3012ee590269f8a25b276b18dca0626c6a0", + "fact_row_count": 12368 + }, + "source_period": 2026, + "target_period": 2027, + "source_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2025-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2025q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:86ce22ba42a6e2a36b889f9b", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2025-04-01", + "end_date": "2025-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2025-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2025q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:434e7b2736b4b16589034c72", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2025-07-01", + "end_date": "2025-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 1 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2025-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2025q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:bc87e53157eaf53c846879dc", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2025-10-01", + "end_date": "2025-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 2 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:78e735569814069f8066ba9f", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-01-01", + "end_date": "2026-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 3 + } + ], + "target_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-04-01", + "end_date": "2026-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 0, + "projection_offset": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2026-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 1, + "projection_offset": 1 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-10-01", + "end_date": "2026-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 2, + "projection_offset": 2 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2027-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2027q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2027-01-01", + "end_date": "2027-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 3, + "projection_offset": 3 + } + ], + "growth": { + "selector": { + "source_name": "treasury", + "source_measure_id": "average_weekly_earnings_annual_percentage_change", + "source_concept": "treasury.prefu26.average_weekly_earnings_annual_percentage_change", + "entity_name": "government", + "period_type": "fiscal_year", + "period_value": 2026, + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "treasury.prefu26.average_weekly_earnings.fy2026", + "dimensions": [ + "earnings_component", + "publication_status" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "publication_status": "Forecast" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:aba7d3d54489b068116e6a25", + "assertion": "source_projection", + "source_table": "PREFU 2026 Table 4.1 - Key economic assumptions for fiscal forecasts" + }, + "validation": { + "unit": "percent", + "source_sha256": "e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9", + "source_vintage": "prefu_2026_09_29", + "entity_role": "forecaster", + "provenance_class": "model_output", + "assertion": "source_projection", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2027-06-30" + }, + "source_column_id": "year_ending_30_june_2027" + }, + "unit_multiplier": "0.01", + "scenario_delta": "-0.5", + "assertion_policy": "allow_source_projection", + "sensitivity_provenance": [ + "d1599: symmetric ±0.5 percentage-point Microcosm sensitivity; high is not a Treasury forecast path" + ] + }, + "subperiods_per_year": 4, + "projection_policy": "observed_preferred_latest_contiguous", + "rounding": { + "digits": 6, + "mode": "nearest_half_even" + }, + "expected_operational_factor": { + "numerator": 1026690, + "denominator": 1000000 + }, + "validity": { + "declared_on": "2026-10-23", + "valid_from": "2026-10-23", + "valid_until": "2026-11-03", + "as_of": "2026-10-23" + }, + "evidence": [ + "Stats NZ QES Table 7; QEMQ.SBSZ9A; ordinary-time FTE, all sectors, actual", + "Treasury PREFU 2026 Table 4.1; year ending 30 June 2027; Chronicle period 2026" + ], + "decision_references": [ + "d1599; IRD band ageing factors, 22 October 2026", + "G8-r2 hub rulings 3, 23 October 2026, sections 1–2" + ] + }, + "operational_factor": { + "numerator": 1026690, + "denominator": 1000000 + }, + "receipt": { + "schema_version": 1, + "operation": "projected_period_average_ratio", + "operation_version": 1, + "factor_id": "p1_ty2026_to_ty2027_low", + "declaration_sha256": "53b22d7900851e896cf6ac17c9ca9f9bb92d436637459b8b59c513397833813e", + "feed_identity": { + "source_commit": "8f4cfd573c0d6f580d9c51cd67e8f449a42ed174", + "facts_sha256": "03f5ea0fe60e6dc93a135ab32cd5cd6ffb56388e702a4734cdb515a956e36b18", + "manifest_sha256": "1ea2d1eeb122275a9ce7f861ca3e20e21dd695772c04ce9d71114168e92fe053", + "artifact_schema_version": "policyengine_ledger.consumer_artifact.v2", + "consumer_fact_schema_versions": [ + "chronicle.consumer_fact.v4" + ], + "consumer_fact_schema_sha256": "d953306d6f01fbc64db4372d0262d3012ee590269f8a25b276b18dca0626c6a0", + "fact_row_count": 12368 + }, + "source_period": 2026, + "target_period": 2027, + "source_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2025-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2025q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:86ce22ba42a6e2a36b889f9b", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:86ce22ba42a6e2a36b889f9b", + "decimal_operand": "1652.72", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1652.72", + "recovered_decimal": "1652.72", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2025-04-01", + "end_date": "2025-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2025-Q2" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2025-06-30", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Jun 2025 quarter", + "start_date": "2025-04-01" + }, + "weight": "1", + "sequence": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2025-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2025q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:434e7b2736b4b16589034c72", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:434e7b2736b4b16589034c72", + "decimal_operand": "1661.12", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1661.12", + "recovered_decimal": "1661.12", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2025-07-01", + "end_date": "2025-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2025-Q3" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2025-09-30", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Sep 2025 quarter", + "start_date": "2025-07-01" + }, + "weight": "1", + "sequence": 1 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2025-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2025q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:bc87e53157eaf53c846879dc", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:bc87e53157eaf53c846879dc", + "decimal_operand": "1681.06", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1681.06", + "recovered_decimal": "1681.06", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2025-10-01", + "end_date": "2025-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2025-Q4" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2025-12-31", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Dec 2025 quarter", + "start_date": "2025-10-01" + }, + "weight": "1", + "sequence": 2 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:78e735569814069f8066ba9f", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:78e735569814069f8066ba9f", + "decimal_operand": "1685.39", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1685.39", + "recovered_decimal": "1685.39", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-01-01", + "end_date": "2026-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2026-Q1" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2026-03-31", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Mar 2026 quarter", + "start_date": "2026-01-01" + }, + "weight": "1", + "sequence": 3 + } + ], + "target_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2", + "decimal_operand": "1700.68", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1700.68", + "recovered_decimal": "1700.68", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-04-01", + "end_date": "2026-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2026-Q2" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2026-06-30", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Jun 2026 quarter", + "start_date": "2026-04-01" + }, + "status": "observed", + "sequence": 0, + "weight": "1", + "projection_offset": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2026-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "status": "projected", + "sequence": 1, + "weight": "1", + "projection_offset": 1, + "projection_anchor": { + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2" + } + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-10-01", + "end_date": "2026-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "status": "projected", + "sequence": 2, + "weight": "1", + "projection_offset": 2, + "projection_anchor": { + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2" + } + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2027-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2027q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2027-01-01", + "end_date": "2027-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "status": "projected", + "sequence": 3, + "weight": "1", + "projection_offset": 3, + "projection_anchor": { + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2" + } + } + ], + "subperiods_per_year": 4, + "projection_policy": "observed_preferred_latest_contiguous", + "denominator_mean": { + "numerator": 668029, + "denominator": 400 + }, + "growth": { + "selector": { + "source_name": "treasury", + "source_measure_id": "average_weekly_earnings_annual_percentage_change", + "source_concept": "treasury.prefu26.average_weekly_earnings_annual_percentage_change", + "entity_name": "government", + "period_type": "fiscal_year", + "period_value": 2026, + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "treasury.prefu26.average_weekly_earnings.fy2026", + "dimensions": [ + "earnings_component", + "publication_status" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "publication_status": "Forecast" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:aba7d3d54489b068116e6a25", + "assertion": "source_projection", + "source_table": "PREFU 2026 Table 4.1 - Key economic assumptions for fiscal forecasts" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:aba7d3d54489b068116e6a25", + "decimal_operand": "2.7", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "2.7", + "recovered_decimal": "2.7", + "decimal_values_equal": true + }, + "validation": { + "unit": "percent", + "source_sha256": "e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9", + "source_vintage": "prefu_2026_09_29", + "entity_role": "forecaster", + "provenance_class": "model_output", + "assertion": "source_projection", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2027-06-30" + }, + "source_column_id": "year_ending_30_june_2027" + }, + "period": { + "type": "fiscal_year", + "value": 2026 + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/treasury/treasury-prefu26-economic-assumptions/2026/e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9/prefu26-charts-data.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/treasury/treasury-prefu26-economic-assumptions/2026/e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9/prefu26-charts-data.xlsx", + "source_file": "prefu26-charts-data.xlsx", + "source_name": "treasury", + "source_sha256": "e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9", + "source_size_bytes": 664670, + "source_table": "PREFU 2026 Table 4.1 - Key economic assumptions for fiscal forecasts", + "url": "https://www.treasury.govt.nz/sites/default/files/2026-09/prefu26-charts-data.xlsx", + "vintage": "prefu_2026_09_29" + }, + "unit": "percent", + "assertion": "source_projection", + "provenance_class": "model_output", + "entity_role": "forecaster", + "period_coverage": { + "basis": "fiscal", + "end_date": "2027-06-30", + "notes": "Publisher Year ending 30 June 2027; Chronicle opening-year fiscal_year:2026. Publisher column status is Forecast. Footnote 3 (Table 4.1!C21:D21): Ordinary time. Footnote 4 (C22:D22): Measures are actuals except for real GDP and nominal GDP. 2026 earnings are therefore actuals despite the Forecast4 column heading; only 2027–2031 earnings are forecasts. The published annual percentage change is retained unchanged; no annual average, level, factor or period alignment is calculated.", + "source_period_label": "Year ending 30 June 2027", + "start_date": "2026-07-01" + }, + "unit_multiplier": "0.01", + "scenario_delta": "-0.5", + "sensitivity_provenance": [ + "d1599: symmetric ±0.5 percentage-point Microcosm sensitivity; high is not a Treasury forecast path" + ], + "effective_growth": { + "numerator": 11, + "denominator": 500 + } + }, + "root_bounds": { + "lower": { + "numerator": 50272759932393, + "denominator": 50000000000000 + }, + "upper": { + "numerator": 100545519864787, + "denominator": 100000000000000 + }, + "exact": false, + "decimal_places": 14, + "degree": 4, + "radicand": { + "numerator": 511, + "denominator": 500 + } + }, + "numerator_mean_bounds": { + "lower": { + "numerator": 21433087509261019469100089110351817978329164269, + "denominator": 12500000000000000000000000000000000000000000 + }, + "upper": { + "numerator": 171464700074090725365853333139754513013059781351, + "denominator": 100000000000000000000000000000000000000000000 + } + }, + "unrounded_factor_bounds": { + "lower": { + "numerator": 21433087509261019469100089110351817978329164269, + "denominator": 20875906250000000000000000000000000000000000000 + }, + "upper": { + "numerator": 171464700074090725365853333139754513013059781351, + "denominator": 167007250000000000000000000000000000000000000000 + } + }, + "rounding": { + "digits": 6, + "mode": "nearest_half_even" + }, + "operational_factor": { + "numerator": 1026690, + "denominator": 1000000 + }, + "validity": { + "declared_on": "2026-10-23", + "valid_from": "2026-10-23", + "valid_until": "2026-11-03", + "as_of": "2026-10-23" + }, + "evidence": [ + "Stats NZ QES Table 7; QEMQ.SBSZ9A; ordinary-time FTE, all sectors, actual", + "Treasury PREFU 2026 Table 4.1; year ending 30 June 2027; Chronicle period 2026" + ], + "decision_references": [ + "d1599; IRD band ageing factors, 22 October 2026", + "G8-r2 hub rulings 3, 23 October 2026, sections 1–2" + ], + "receipt_sha256": "28006b689061f41a7cfd9abf055aa3ee1ee5a5414d83fa255d36d5ee7fe326bc" + } + }, + { + "declaration": { + "schema_version": 1, + "id": "p1_ty2026_to_ty2027_central", + "operation": "projected_period_average_ratio", + "operation_version": 1, + "purpose": "profile_input", + "feed_identity": { + "source_commit": "8f4cfd573c0d6f580d9c51cd67e8f449a42ed174", + "facts_sha256": "03f5ea0fe60e6dc93a135ab32cd5cd6ffb56388e702a4734cdb515a956e36b18", + "manifest_sha256": "1ea2d1eeb122275a9ce7f861ca3e20e21dd695772c04ce9d71114168e92fe053", + "artifact_schema_version": "policyengine_ledger.consumer_artifact.v2", + "consumer_fact_schema_versions": [ + "chronicle.consumer_fact.v4" + ], + "consumer_fact_schema_sha256": "d953306d6f01fbc64db4372d0262d3012ee590269f8a25b276b18dca0626c6a0", + "fact_row_count": 12368 + }, + "source_period": 2026, + "target_period": 2027, + "source_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2025-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2025q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:86ce22ba42a6e2a36b889f9b", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2025-04-01", + "end_date": "2025-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2025-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2025q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:434e7b2736b4b16589034c72", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2025-07-01", + "end_date": "2025-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 1 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2025-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2025q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:bc87e53157eaf53c846879dc", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2025-10-01", + "end_date": "2025-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 2 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:78e735569814069f8066ba9f", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-01-01", + "end_date": "2026-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 3 + } + ], + "target_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-04-01", + "end_date": "2026-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 0, + "projection_offset": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2026-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 1, + "projection_offset": 1 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-10-01", + "end_date": "2026-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 2, + "projection_offset": 2 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2027-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2027q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2027-01-01", + "end_date": "2027-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 3, + "projection_offset": 3 + } + ], + "growth": { + "selector": { + "source_name": "treasury", + "source_measure_id": "average_weekly_earnings_annual_percentage_change", + "source_concept": "treasury.prefu26.average_weekly_earnings_annual_percentage_change", + "entity_name": "government", + "period_type": "fiscal_year", + "period_value": 2026, + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "treasury.prefu26.average_weekly_earnings.fy2026", + "dimensions": [ + "earnings_component", + "publication_status" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "publication_status": "Forecast" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:aba7d3d54489b068116e6a25", + "assertion": "source_projection", + "source_table": "PREFU 2026 Table 4.1 - Key economic assumptions for fiscal forecasts" + }, + "validation": { + "unit": "percent", + "source_sha256": "e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9", + "source_vintage": "prefu_2026_09_29", + "entity_role": "forecaster", + "provenance_class": "model_output", + "assertion": "source_projection", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2027-06-30" + }, + "source_column_id": "year_ending_30_june_2027" + }, + "unit_multiplier": "0.01", + "scenario_delta": "0", + "assertion_policy": "allow_source_projection", + "sensitivity_provenance": [ + "d1599: symmetric ±0.5 percentage-point Microcosm sensitivity; high is not a Treasury forecast path" + ] + }, + "subperiods_per_year": 4, + "projection_policy": "observed_preferred_latest_contiguous", + "rounding": { + "digits": 6, + "mode": "nearest_half_even" + }, + "expected_operational_factor": { + "numerator": 1028580, + "denominator": 1000000 + }, + "validity": { + "declared_on": "2026-10-23", + "valid_from": "2026-10-23", + "valid_until": "2026-11-03", + "as_of": "2026-10-23" + }, + "evidence": [ + "Stats NZ QES Table 7; QEMQ.SBSZ9A; ordinary-time FTE, all sectors, actual", + "Treasury PREFU 2026 Table 4.1; year ending 30 June 2027; Chronicle period 2026" + ], + "decision_references": [ + "d1599; IRD band ageing factors, 22 October 2026", + "G8-r2 hub rulings 3, 23 October 2026, sections 1–2" + ] + }, + "operational_factor": { + "numerator": 1028580, + "denominator": 1000000 + }, + "receipt": { + "schema_version": 1, + "operation": "projected_period_average_ratio", + "operation_version": 1, + "factor_id": "p1_ty2026_to_ty2027_central", + "declaration_sha256": "8507fe3d665c6a0727319765827120afda4ee36e8d970a70b61a747d8cb9210b", + "feed_identity": { + "source_commit": "8f4cfd573c0d6f580d9c51cd67e8f449a42ed174", + "facts_sha256": "03f5ea0fe60e6dc93a135ab32cd5cd6ffb56388e702a4734cdb515a956e36b18", + "manifest_sha256": "1ea2d1eeb122275a9ce7f861ca3e20e21dd695772c04ce9d71114168e92fe053", + "artifact_schema_version": "policyengine_ledger.consumer_artifact.v2", + "consumer_fact_schema_versions": [ + "chronicle.consumer_fact.v4" + ], + "consumer_fact_schema_sha256": "d953306d6f01fbc64db4372d0262d3012ee590269f8a25b276b18dca0626c6a0", + "fact_row_count": 12368 + }, + "source_period": 2026, + "target_period": 2027, + "source_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2025-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2025q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:86ce22ba42a6e2a36b889f9b", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:86ce22ba42a6e2a36b889f9b", + "decimal_operand": "1652.72", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1652.72", + "recovered_decimal": "1652.72", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2025-04-01", + "end_date": "2025-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2025-Q2" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2025-06-30", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Jun 2025 quarter", + "start_date": "2025-04-01" + }, + "weight": "1", + "sequence": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2025-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2025q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:434e7b2736b4b16589034c72", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:434e7b2736b4b16589034c72", + "decimal_operand": "1661.12", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1661.12", + "recovered_decimal": "1661.12", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2025-07-01", + "end_date": "2025-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2025-Q3" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2025-09-30", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Sep 2025 quarter", + "start_date": "2025-07-01" + }, + "weight": "1", + "sequence": 1 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2025-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2025q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:bc87e53157eaf53c846879dc", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:bc87e53157eaf53c846879dc", + "decimal_operand": "1681.06", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1681.06", + "recovered_decimal": "1681.06", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2025-10-01", + "end_date": "2025-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2025-Q4" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2025-12-31", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Dec 2025 quarter", + "start_date": "2025-10-01" + }, + "weight": "1", + "sequence": 2 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:78e735569814069f8066ba9f", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:78e735569814069f8066ba9f", + "decimal_operand": "1685.39", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1685.39", + "recovered_decimal": "1685.39", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-01-01", + "end_date": "2026-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2026-Q1" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2026-03-31", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Mar 2026 quarter", + "start_date": "2026-01-01" + }, + "weight": "1", + "sequence": 3 + } + ], + "target_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2", + "decimal_operand": "1700.68", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1700.68", + "recovered_decimal": "1700.68", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-04-01", + "end_date": "2026-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2026-Q2" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2026-06-30", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Jun 2026 quarter", + "start_date": "2026-04-01" + }, + "status": "observed", + "sequence": 0, + "weight": "1", + "projection_offset": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2026-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "status": "projected", + "sequence": 1, + "weight": "1", + "projection_offset": 1, + "projection_anchor": { + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2" + } + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-10-01", + "end_date": "2026-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "status": "projected", + "sequence": 2, + "weight": "1", + "projection_offset": 2, + "projection_anchor": { + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2" + } + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2027-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2027q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2027-01-01", + "end_date": "2027-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "status": "projected", + "sequence": 3, + "weight": "1", + "projection_offset": 3, + "projection_anchor": { + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2" + } + } + ], + "subperiods_per_year": 4, + "projection_policy": "observed_preferred_latest_contiguous", + "denominator_mean": { + "numerator": 668029, + "denominator": 400 + }, + "growth": { + "selector": { + "source_name": "treasury", + "source_measure_id": "average_weekly_earnings_annual_percentage_change", + "source_concept": "treasury.prefu26.average_weekly_earnings_annual_percentage_change", + "entity_name": "government", + "period_type": "fiscal_year", + "period_value": 2026, + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "treasury.prefu26.average_weekly_earnings.fy2026", + "dimensions": [ + "earnings_component", + "publication_status" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "publication_status": "Forecast" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:aba7d3d54489b068116e6a25", + "assertion": "source_projection", + "source_table": "PREFU 2026 Table 4.1 - Key economic assumptions for fiscal forecasts" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:aba7d3d54489b068116e6a25", + "decimal_operand": "2.7", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "2.7", + "recovered_decimal": "2.7", + "decimal_values_equal": true + }, + "validation": { + "unit": "percent", + "source_sha256": "e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9", + "source_vintage": "prefu_2026_09_29", + "entity_role": "forecaster", + "provenance_class": "model_output", + "assertion": "source_projection", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2027-06-30" + }, + "source_column_id": "year_ending_30_june_2027" + }, + "period": { + "type": "fiscal_year", + "value": 2026 + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/treasury/treasury-prefu26-economic-assumptions/2026/e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9/prefu26-charts-data.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/treasury/treasury-prefu26-economic-assumptions/2026/e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9/prefu26-charts-data.xlsx", + "source_file": "prefu26-charts-data.xlsx", + "source_name": "treasury", + "source_sha256": "e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9", + "source_size_bytes": 664670, + "source_table": "PREFU 2026 Table 4.1 - Key economic assumptions for fiscal forecasts", + "url": "https://www.treasury.govt.nz/sites/default/files/2026-09/prefu26-charts-data.xlsx", + "vintage": "prefu_2026_09_29" + }, + "unit": "percent", + "assertion": "source_projection", + "provenance_class": "model_output", + "entity_role": "forecaster", + "period_coverage": { + "basis": "fiscal", + "end_date": "2027-06-30", + "notes": "Publisher Year ending 30 June 2027; Chronicle opening-year fiscal_year:2026. Publisher column status is Forecast. Footnote 3 (Table 4.1!C21:D21): Ordinary time. Footnote 4 (C22:D22): Measures are actuals except for real GDP and nominal GDP. 2026 earnings are therefore actuals despite the Forecast4 column heading; only 2027–2031 earnings are forecasts. The published annual percentage change is retained unchanged; no annual average, level, factor or period alignment is calculated.", + "source_period_label": "Year ending 30 June 2027", + "start_date": "2026-07-01" + }, + "unit_multiplier": "0.01", + "scenario_delta": "0", + "sensitivity_provenance": [ + "d1599: symmetric ±0.5 percentage-point Microcosm sensitivity; high is not a Treasury forecast path" + ], + "effective_growth": { + "numerator": 27, + "denominator": 1000 + } + }, + "root_bounds": { + "lower": { + "numerator": 100668271307927, + "denominator": 100000000000000 + }, + "upper": { + "numerator": 12583533913491, + "denominator": 12500000000000 + }, + "exact": false, + "decimal_places": 14, + "degree": 4, + "radicand": { + "numerator": 1027, + "denominator": 1000 + } + }, + "numerator_mean_bounds": { + "lower": { + "numerator": 171780381168748188591006079321988113950935096211, + "denominator": 100000000000000000000000000000000000000000000 + }, + "upper": { + "numerator": 335508556970216332809143747140199880884173607, + "denominator": 195312500000000000000000000000000000000000 + } + }, + "unrounded_factor_bounds": { + "lower": { + "numerator": 171780381168748188591006079321988113950935096211, + "denominator": 167007250000000000000000000000000000000000000000 + }, + "upper": { + "numerator": 335508556970216332809143747140199880884173607, + "denominator": 326186035156250000000000000000000000000000000 + } + }, + "rounding": { + "digits": 6, + "mode": "nearest_half_even" + }, + "operational_factor": { + "numerator": 1028580, + "denominator": 1000000 + }, + "validity": { + "declared_on": "2026-10-23", + "valid_from": "2026-10-23", + "valid_until": "2026-11-03", + "as_of": "2026-10-23" + }, + "evidence": [ + "Stats NZ QES Table 7; QEMQ.SBSZ9A; ordinary-time FTE, all sectors, actual", + "Treasury PREFU 2026 Table 4.1; year ending 30 June 2027; Chronicle period 2026" + ], + "decision_references": [ + "d1599; IRD band ageing factors, 22 October 2026", + "G8-r2 hub rulings 3, 23 October 2026, sections 1–2" + ], + "receipt_sha256": "43e1914b11969709ea4f0df7fc9a8280f82f15bf186b4e107f986da0a7871c53" + } + }, + { + "declaration": { + "schema_version": 1, + "id": "p1_ty2026_to_ty2027_high", + "operation": "projected_period_average_ratio", + "operation_version": 1, + "purpose": "profile_input", + "feed_identity": { + "source_commit": "8f4cfd573c0d6f580d9c51cd67e8f449a42ed174", + "facts_sha256": "03f5ea0fe60e6dc93a135ab32cd5cd6ffb56388e702a4734cdb515a956e36b18", + "manifest_sha256": "1ea2d1eeb122275a9ce7f861ca3e20e21dd695772c04ce9d71114168e92fe053", + "artifact_schema_version": "policyengine_ledger.consumer_artifact.v2", + "consumer_fact_schema_versions": [ + "chronicle.consumer_fact.v4" + ], + "consumer_fact_schema_sha256": "d953306d6f01fbc64db4372d0262d3012ee590269f8a25b276b18dca0626c6a0", + "fact_row_count": 12368 + }, + "source_period": 2026, + "target_period": 2027, + "source_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2025-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2025q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:86ce22ba42a6e2a36b889f9b", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2025-04-01", + "end_date": "2025-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2025-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2025q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:434e7b2736b4b16589034c72", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2025-07-01", + "end_date": "2025-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 1 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2025-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2025q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:bc87e53157eaf53c846879dc", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2025-10-01", + "end_date": "2025-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 2 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:78e735569814069f8066ba9f", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-01-01", + "end_date": "2026-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 3 + } + ], + "target_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-04-01", + "end_date": "2026-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 0, + "projection_offset": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2026-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 1, + "projection_offset": 1 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-10-01", + "end_date": "2026-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 2, + "projection_offset": 2 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2027-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2027q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2027-01-01", + "end_date": "2027-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 3, + "projection_offset": 3 + } + ], + "growth": { + "selector": { + "source_name": "treasury", + "source_measure_id": "average_weekly_earnings_annual_percentage_change", + "source_concept": "treasury.prefu26.average_weekly_earnings_annual_percentage_change", + "entity_name": "government", + "period_type": "fiscal_year", + "period_value": 2026, + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "treasury.prefu26.average_weekly_earnings.fy2026", + "dimensions": [ + "earnings_component", + "publication_status" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "publication_status": "Forecast" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:aba7d3d54489b068116e6a25", + "assertion": "source_projection", + "source_table": "PREFU 2026 Table 4.1 - Key economic assumptions for fiscal forecasts" + }, + "validation": { + "unit": "percent", + "source_sha256": "e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9", + "source_vintage": "prefu_2026_09_29", + "entity_role": "forecaster", + "provenance_class": "model_output", + "assertion": "source_projection", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2027-06-30" + }, + "source_column_id": "year_ending_30_june_2027" + }, + "unit_multiplier": "0.01", + "scenario_delta": "0.5", + "assertion_policy": "allow_source_projection", + "sensitivity_provenance": [ + "d1599: symmetric ±0.5 percentage-point Microcosm sensitivity; high is not a Treasury forecast path" + ] + }, + "subperiods_per_year": 4, + "projection_policy": "observed_preferred_latest_contiguous", + "rounding": { + "digits": 6, + "mode": "nearest_half_even" + }, + "expected_operational_factor": { + "numerator": 1030467, + "denominator": 1000000 + }, + "validity": { + "declared_on": "2026-10-23", + "valid_from": "2026-10-23", + "valid_until": "2026-11-03", + "as_of": "2026-10-23" + }, + "evidence": [ + "Stats NZ QES Table 7; QEMQ.SBSZ9A; ordinary-time FTE, all sectors, actual", + "Treasury PREFU 2026 Table 4.1; year ending 30 June 2027; Chronicle period 2026" + ], + "decision_references": [ + "d1599; IRD band ageing factors, 22 October 2026", + "G8-r2 hub rulings 3, 23 October 2026, sections 1–2" + ] + }, + "operational_factor": { + "numerator": 1030467, + "denominator": 1000000 + }, + "receipt": { + "schema_version": 1, + "operation": "projected_period_average_ratio", + "operation_version": 1, + "factor_id": "p1_ty2026_to_ty2027_high", + "declaration_sha256": "b2c2c0b631861fa6c3975dfb6d326b49ca60b81057abc5bb8581dd0c0bca1ce9", + "feed_identity": { + "source_commit": "8f4cfd573c0d6f580d9c51cd67e8f449a42ed174", + "facts_sha256": "03f5ea0fe60e6dc93a135ab32cd5cd6ffb56388e702a4734cdb515a956e36b18", + "manifest_sha256": "1ea2d1eeb122275a9ce7f861ca3e20e21dd695772c04ce9d71114168e92fe053", + "artifact_schema_version": "policyengine_ledger.consumer_artifact.v2", + "consumer_fact_schema_versions": [ + "chronicle.consumer_fact.v4" + ], + "consumer_fact_schema_sha256": "d953306d6f01fbc64db4372d0262d3012ee590269f8a25b276b18dca0626c6a0", + "fact_row_count": 12368 + }, + "source_period": 2026, + "target_period": 2027, + "source_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2025-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2025q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:86ce22ba42a6e2a36b889f9b", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:86ce22ba42a6e2a36b889f9b", + "decimal_operand": "1652.72", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1652.72", + "recovered_decimal": "1652.72", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2025-04-01", + "end_date": "2025-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2025-Q2" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2025-06-30", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Jun 2025 quarter", + "start_date": "2025-04-01" + }, + "weight": "1", + "sequence": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2025-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2025q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:434e7b2736b4b16589034c72", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:434e7b2736b4b16589034c72", + "decimal_operand": "1661.12", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1661.12", + "recovered_decimal": "1661.12", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2025-07-01", + "end_date": "2025-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2025-Q3" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2025-09-30", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Sep 2025 quarter", + "start_date": "2025-07-01" + }, + "weight": "1", + "sequence": 1 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2025-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2025q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:bc87e53157eaf53c846879dc", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:bc87e53157eaf53c846879dc", + "decimal_operand": "1681.06", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1681.06", + "recovered_decimal": "1681.06", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2025-10-01", + "end_date": "2025-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2025-Q4" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2025-12-31", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Dec 2025 quarter", + "start_date": "2025-10-01" + }, + "weight": "1", + "sequence": 2 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:78e735569814069f8066ba9f", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:78e735569814069f8066ba9f", + "decimal_operand": "1685.39", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1685.39", + "recovered_decimal": "1685.39", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-01-01", + "end_date": "2026-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2026-Q1" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2026-03-31", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Mar 2026 quarter", + "start_date": "2026-01-01" + }, + "weight": "1", + "sequence": 3 + } + ], + "target_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2", + "decimal_operand": "1700.68", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1700.68", + "recovered_decimal": "1700.68", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-04-01", + "end_date": "2026-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2026-Q2" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2026-06-30", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Jun 2026 quarter", + "start_date": "2026-04-01" + }, + "status": "observed", + "sequence": 0, + "weight": "1", + "projection_offset": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2026-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "status": "projected", + "sequence": 1, + "weight": "1", + "projection_offset": 1, + "projection_anchor": { + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2" + } + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-10-01", + "end_date": "2026-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "status": "projected", + "sequence": 2, + "weight": "1", + "projection_offset": 2, + "projection_anchor": { + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2" + } + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2027-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2027q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2027-01-01", + "end_date": "2027-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "status": "projected", + "sequence": 3, + "weight": "1", + "projection_offset": 3, + "projection_anchor": { + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2" + } + } + ], + "subperiods_per_year": 4, + "projection_policy": "observed_preferred_latest_contiguous", + "denominator_mean": { + "numerator": 668029, + "denominator": 400 + }, + "growth": { + "selector": { + "source_name": "treasury", + "source_measure_id": "average_weekly_earnings_annual_percentage_change", + "source_concept": "treasury.prefu26.average_weekly_earnings_annual_percentage_change", + "entity_name": "government", + "period_type": "fiscal_year", + "period_value": 2026, + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "treasury.prefu26.average_weekly_earnings.fy2026", + "dimensions": [ + "earnings_component", + "publication_status" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "publication_status": "Forecast" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:aba7d3d54489b068116e6a25", + "assertion": "source_projection", + "source_table": "PREFU 2026 Table 4.1 - Key economic assumptions for fiscal forecasts" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:aba7d3d54489b068116e6a25", + "decimal_operand": "2.7", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "2.7", + "recovered_decimal": "2.7", + "decimal_values_equal": true + }, + "validation": { + "unit": "percent", + "source_sha256": "e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9", + "source_vintage": "prefu_2026_09_29", + "entity_role": "forecaster", + "provenance_class": "model_output", + "assertion": "source_projection", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2027-06-30" + }, + "source_column_id": "year_ending_30_june_2027" + }, + "period": { + "type": "fiscal_year", + "value": 2026 + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/treasury/treasury-prefu26-economic-assumptions/2026/e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9/prefu26-charts-data.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/treasury/treasury-prefu26-economic-assumptions/2026/e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9/prefu26-charts-data.xlsx", + "source_file": "prefu26-charts-data.xlsx", + "source_name": "treasury", + "source_sha256": "e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9", + "source_size_bytes": 664670, + "source_table": "PREFU 2026 Table 4.1 - Key economic assumptions for fiscal forecasts", + "url": "https://www.treasury.govt.nz/sites/default/files/2026-09/prefu26-charts-data.xlsx", + "vintage": "prefu_2026_09_29" + }, + "unit": "percent", + "assertion": "source_projection", + "provenance_class": "model_output", + "entity_role": "forecaster", + "period_coverage": { + "basis": "fiscal", + "end_date": "2027-06-30", + "notes": "Publisher Year ending 30 June 2027; Chronicle opening-year fiscal_year:2026. Publisher column status is Forecast. Footnote 3 (Table 4.1!C21:D21): Ordinary time. Footnote 4 (C22:D22): Measures are actuals except for real GDP and nominal GDP. 2026 earnings are therefore actuals despite the Forecast4 column heading; only 2027–2031 earnings are forecasts. The published annual percentage change is retained unchanged; no annual average, level, factor or period alignment is calculated.", + "source_period_label": "Year ending 30 June 2027", + "start_date": "2026-07-01" + }, + "unit_multiplier": "0.01", + "scenario_delta": "0.5", + "sensitivity_provenance": [ + "d1599: symmetric ±0.5 percentage-point Microcosm sensitivity; high is not a Treasury forecast path" + ], + "effective_growth": { + "numerator": 4, + "denominator": 125 + } + }, + "root_bounds": { + "lower": { + "numerator": 100790575349881, + "denominator": 100000000000000 + }, + "upper": { + "numerator": 50395287674941, + "denominator": 50000000000000 + }, + "exact": false, + "decimal_places": 14, + "degree": 4, + "radicand": { + "numerator": 129, + "denominator": 125 + } + }, + "numerator_mean_bounds": { + "lower": { + "numerator": 172095423947015521933465483088234150187075944797, + "denominator": 100000000000000000000000000000000000000000000 + }, + "upper": { + "numerator": 21511927993377262490437471966978882670435826057, + "denominator": 12500000000000000000000000000000000000000000 + } + }, + "unrounded_factor_bounds": { + "lower": { + "numerator": 172095423947015521933465483088234150187075944797, + "denominator": 167007250000000000000000000000000000000000000000 + }, + "upper": { + "numerator": 21511927993377262490437471966978882670435826057, + "denominator": 20875906250000000000000000000000000000000000000 + } + }, + "rounding": { + "digits": 6, + "mode": "nearest_half_even" + }, + "operational_factor": { + "numerator": 1030467, + "denominator": 1000000 + }, + "validity": { + "declared_on": "2026-10-23", + "valid_from": "2026-10-23", + "valid_until": "2026-11-03", + "as_of": "2026-10-23" + }, + "evidence": [ + "Stats NZ QES Table 7; QEMQ.SBSZ9A; ordinary-time FTE, all sectors, actual", + "Treasury PREFU 2026 Table 4.1; year ending 30 June 2027; Chronicle period 2026" + ], + "decision_references": [ + "d1599; IRD band ageing factors, 22 October 2026", + "G8-r2 hub rulings 3, 23 October 2026, sections 1–2" + ], + "receipt_sha256": "5d1b1006938c2628d7f2d4344643270c2477f01cd0d313bfa323834ff8e54cb2" + } + }, + { + "declaration": { + "schema_version": 1, + "id": "observed_ty2026_over_ty2024", + "operation": "projected_period_average_ratio", + "operation_version": 1, + "purpose": "diagnostic", + "feed_identity": { + "source_commit": "8f4cfd573c0d6f580d9c51cd67e8f449a42ed174", + "facts_sha256": "03f5ea0fe60e6dc93a135ab32cd5cd6ffb56388e702a4734cdb515a956e36b18", + "manifest_sha256": "1ea2d1eeb122275a9ce7f861ca3e20e21dd695772c04ce9d71114168e92fe053", + "artifact_schema_version": "policyengine_ledger.consumer_artifact.v2", + "consumer_fact_schema_versions": [ + "chronicle.consumer_fact.v4" + ], + "consumer_fact_schema_sha256": "d953306d6f01fbc64db4372d0262d3012ee590269f8a25b276b18dca0626c6a0", + "fact_row_count": 12368 + }, + "source_period": 2024, + "target_period": 2026, + "source_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2023-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2023q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:cfbce1343aa646fce0411631", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2023-04-01", + "end_date": "2023-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2023-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2023q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:f9f4f127d080fe2f06617947", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2023-07-01", + "end_date": "2023-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 1 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2023-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2023q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:fb5938810b7b37ae8534e5f7", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2023-10-01", + "end_date": "2023-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 2 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2024-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2024q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:854e91ec1dd95e396659ad8e", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2024-01-01", + "end_date": "2024-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 3 + } + ], + "target_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2025-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2025q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:86ce22ba42a6e2a36b889f9b", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2025-04-01", + "end_date": "2025-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 0, + "projection_offset": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2025-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2025q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:434e7b2736b4b16589034c72", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2025-07-01", + "end_date": "2025-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 1, + "projection_offset": 1 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2025-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2025q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:bc87e53157eaf53c846879dc", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2025-10-01", + "end_date": "2025-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 2, + "projection_offset": 2 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:78e735569814069f8066ba9f", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-01-01", + "end_date": "2026-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 3, + "projection_offset": 3 + } + ], + "growth": null, + "subperiods_per_year": 4, + "projection_policy": "observed_preferred_latest_contiguous", + "rounding": { + "digits": 6, + "mode": "nearest_half_even" + }, + "expected_operational_factor": { + "numerator": 1086069, + "denominator": 1000000 + }, + "validity": { + "declared_on": "2026-10-23", + "valid_from": "2026-10-23", + "valid_until": "2026-11-03", + "as_of": "2026-10-23" + }, + "evidence": [ + "Stats NZ QES Table 7; QEMQ.SBSZ9A; ordinary-time FTE, all sectors, actual", + "Treasury PREFU 2026 Table 4.1; year ending 30 June 2027; Chronicle period 2026" + ], + "decision_references": [ + "d1599; IRD band ageing factors, 22 October 2026", + "G8-r2 hub rulings 3, 23 October 2026, sections 1–2" + ] + }, + "operational_factor": { + "numerator": 1086069, + "denominator": 1000000 + }, + "receipt": { + "schema_version": 1, + "operation": "projected_period_average_ratio", + "operation_version": 1, + "factor_id": "observed_ty2026_over_ty2024", + "declaration_sha256": "dae0d44f673fb8e437c4787b3c0d6d6c142412c2fbb29e03f1702a6940a56316", + "feed_identity": { + "source_commit": "8f4cfd573c0d6f580d9c51cd67e8f449a42ed174", + "facts_sha256": "03f5ea0fe60e6dc93a135ab32cd5cd6ffb56388e702a4734cdb515a956e36b18", + "manifest_sha256": "1ea2d1eeb122275a9ce7f861ca3e20e21dd695772c04ce9d71114168e92fe053", + "artifact_schema_version": "policyengine_ledger.consumer_artifact.v2", + "consumer_fact_schema_versions": [ + "chronicle.consumer_fact.v4" + ], + "consumer_fact_schema_sha256": "d953306d6f01fbc64db4372d0262d3012ee590269f8a25b276b18dca0626c6a0", + "fact_row_count": 12368 + }, + "source_period": 2024, + "target_period": 2026, + "source_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2023-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2023q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:cfbce1343aa646fce0411631", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:cfbce1343aa646fce0411631", + "decimal_operand": "1502.27", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1502.27", + "recovered_decimal": "1502.27", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2023-04-01", + "end_date": "2023-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2023-Q2" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2025-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_size_bytes": 135784, + "source_table": "Quarterly Employment Survey June 2025 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2025-quarter/Download-data/quarterly-employment-survey-june-2025-quarter.xlsx", + "vintage": "stats_nz_qes_june_2025" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2023-06-30", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Jun 2023 quarter", + "start_date": "2023-04-01" + }, + "weight": "1", + "sequence": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2023-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2023q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:f9f4f127d080fe2f06617947", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:f9f4f127d080fe2f06617947", + "decimal_operand": "1528.92", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1528.92", + "recovered_decimal": "1528.92", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2023-07-01", + "end_date": "2023-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2023-Q3" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2025-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_size_bytes": 135784, + "source_table": "Quarterly Employment Survey June 2025 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2025-quarter/Download-data/quarterly-employment-survey-june-2025-quarter.xlsx", + "vintage": "stats_nz_qes_june_2025" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2023-09-30", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Sep 2023 quarter", + "start_date": "2023-07-01" + }, + "weight": "1", + "sequence": 1 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2023-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2023q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:fb5938810b7b37ae8534e5f7", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:fb5938810b7b37ae8534e5f7", + "decimal_operand": "1556.16", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1556.16", + "recovered_decimal": "1556.16", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2023-10-01", + "end_date": "2023-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2023-Q4" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2025-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_size_bytes": 135784, + "source_table": "Quarterly Employment Survey June 2025 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2025-quarter/Download-data/quarterly-employment-survey-june-2025-quarter.xlsx", + "vintage": "stats_nz_qes_june_2025" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2023-12-31", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Dec 2023 quarter", + "start_date": "2023-10-01" + }, + "weight": "1", + "sequence": 2 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2024-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2024q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:854e91ec1dd95e396659ad8e", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:854e91ec1dd95e396659ad8e", + "decimal_operand": "1563.54", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1563.54", + "recovered_decimal": "1563.54", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2024-01-01", + "end_date": "2024-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2024-Q1" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2025-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_size_bytes": 135784, + "source_table": "Quarterly Employment Survey June 2025 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2025-quarter/Download-data/quarterly-employment-survey-june-2025-quarter.xlsx", + "vintage": "stats_nz_qes_june_2025" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2024-03-31", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Mar 2024 quarter", + "start_date": "2024-01-01" + }, + "weight": "1", + "sequence": 3 + } + ], + "target_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2025-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2025q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:86ce22ba42a6e2a36b889f9b", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:86ce22ba42a6e2a36b889f9b", + "decimal_operand": "1652.72", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1652.72", + "recovered_decimal": "1652.72", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2025-04-01", + "end_date": "2025-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2025-Q2" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2025-06-30", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Jun 2025 quarter", + "start_date": "2025-04-01" + }, + "status": "observed", + "sequence": 0, + "weight": "1", + "projection_offset": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2025-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2025q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:434e7b2736b4b16589034c72", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:434e7b2736b4b16589034c72", + "decimal_operand": "1661.12", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1661.12", + "recovered_decimal": "1661.12", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2025-07-01", + "end_date": "2025-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2025-Q3" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2025-09-30", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Sep 2025 quarter", + "start_date": "2025-07-01" + }, + "status": "observed", + "sequence": 1, + "weight": "1", + "projection_offset": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2025-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2025q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:bc87e53157eaf53c846879dc", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:bc87e53157eaf53c846879dc", + "decimal_operand": "1681.06", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1681.06", + "recovered_decimal": "1681.06", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2025-10-01", + "end_date": "2025-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2025-Q4" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2025-12-31", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Dec 2025 quarter", + "start_date": "2025-10-01" + }, + "status": "observed", + "sequence": 2, + "weight": "1", + "projection_offset": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:78e735569814069f8066ba9f", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:78e735569814069f8066ba9f", + "decimal_operand": "1685.39", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1685.39", + "recovered_decimal": "1685.39", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-01-01", + "end_date": "2026-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2026-Q1" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2026-03-31", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Mar 2026 quarter", + "start_date": "2026-01-01" + }, + "status": "observed", + "sequence": 3, + "weight": "1", + "projection_offset": 0 + } + ], + "subperiods_per_year": 4, + "projection_policy": "observed_preferred_latest_contiguous", + "denominator_mean": { + "numerator": 615089, + "denominator": 400 + }, + "growth": null, + "root_bounds": { + "lower": { + "numerator": 1, + "denominator": 1 + }, + "upper": { + "numerator": 1, + "denominator": 1 + }, + "exact": true, + "decimal_places": 14, + "degree": 4, + "radicand": { + "numerator": 1, + "denominator": 1 + } + }, + "numerator_mean_bounds": { + "lower": { + "numerator": 668029, + "denominator": 400 + }, + "upper": { + "numerator": 668029, + "denominator": 400 + } + }, + "unrounded_factor_bounds": { + "lower": { + "numerator": 668029, + "denominator": 615089 + }, + "upper": { + "numerator": 668029, + "denominator": 615089 + } + }, + "rounding": { + "digits": 6, + "mode": "nearest_half_even" + }, + "operational_factor": { + "numerator": 1086069, + "denominator": 1000000 + }, + "validity": { + "declared_on": "2026-10-23", + "valid_from": "2026-10-23", + "valid_until": "2026-11-03", + "as_of": "2026-10-23" + }, + "evidence": [ + "Stats NZ QES Table 7; QEMQ.SBSZ9A; ordinary-time FTE, all sectors, actual", + "Treasury PREFU 2026 Table 4.1; year ending 30 June 2027; Chronicle period 2026" + ], + "decision_references": [ + "d1599; IRD band ageing factors, 22 October 2026", + "G8-r2 hub rulings 3, 23 October 2026, sections 1–2" + ], + "receipt_sha256": "6487bd1664ce1e691faaa0771e9835da5437243b06a5c7d749888380a76cd995" + } + } + ] +} diff --git a/packages/microcosm-build/src/microcosm/build/nz/reference_inventory.json b/packages/microcosm-build/src/microcosm/build/nz/reference_inventory.json index bc33e7a400..cb5c0e7635 100644 --- a/packages/microcosm-build/src/microcosm/build/nz/reference_inventory.json +++ b/packages/microcosm-build/src/microcosm/build/nz/reference_inventory.json @@ -36,7 +36,7 @@ "active": 64, "requires_reference_profile_resolution": 170, "requires_census_family_nucleus_standard": 51, - "requires_g8r2_taxable_income_contract_intervals_and_ageing": 474, + "requires_prerequisites": 474, "requires_stratified_receipt_assignment": 12, "requires_receipt_binding": 1, "requires_unsupported_veterans_pension": 1 @@ -14290,7 +14290,7 @@ }, { "id": "qes_prefu_and_b5_ageing", - "reason": "G8-r2; QES/PREFU rows are present in the pinned artifact but unused by this residual-allocation task." + "reason": "QES/PREFU feed rows support the declared factor calculations in reference_aging.json; they never become direct calibration references or carried target values." }, { "id": "wff_tripwires", diff --git a/packages/microcosm-build/src/microcosm/build/nz/reference_profiles.json b/packages/microcosm-build/src/microcosm/build/nz/reference_profiles.json index 4fe3ebf5f4..4c891ca5a7 100644 --- a/packages/microcosm-build/src/microcosm/build/nz/reference_profiles.json +++ b/packages/microcosm-build/src/microcosm/build/nz/reference_profiles.json @@ -1,5 +1,6 @@ { "schema_version": 1, + "factor_context_resource": "reference_aging.json", "dimensions": { "census_residual_allocation": { "default": "mar", @@ -70601,17 +70602,1469 @@ "aging_index": { "default": "central", "profiles": { - "central": { - "transformations": [], - "deferred": true - }, "low": { - "available": false, - "transformations": [] + "transformations": [ + { + "operation": "scale_money_and_intervals", + "factor_id": "ird_ty2024_to_ty2027_low", + "reference_names": [ + "ird_taxable_income_total_by_band__row_8", + "ird_taxable_income_total_by_band__row_9", + "ird_taxable_income_total_by_band__row_10", + "ird_taxable_income_total_by_band__row_11", + "ird_taxable_income_total_by_band__row_12", + "ird_taxable_income_total_by_band__row_13", + "ird_taxable_income_total_by_band__row_14", + "ird_taxable_income_total_by_band__row_15", + "ird_taxable_income_total_by_band__row_16", + "ird_taxable_income_total_by_band__row_17", + "ird_taxable_income_total_by_band__row_18", + "ird_taxable_income_total_by_band__row_19", + "ird_taxable_income_total_by_band__row_20", + "ird_taxable_income_total_by_band__row_21", + "ird_taxable_income_total_by_band__row_22", + "ird_taxable_income_total_by_band__row_23", + "ird_taxable_income_total_by_band__row_24", + "ird_taxable_income_total_by_band__row_25", + "ird_taxable_income_total_by_band__row_26", + "ird_taxable_income_total_by_band__row_27", + "ird_taxable_income_total_by_band__row_28", + "ird_taxable_income_total_by_band__row_29", + "ird_taxable_income_total_by_band__row_30", + "ird_taxable_income_total_by_band__row_31", + "ird_taxable_income_total_by_band__row_32", + "ird_taxable_income_total_by_band__row_33", + "ird_taxable_income_total_by_band__row_34", + "ird_taxable_income_total_by_band__row_35", + "ird_taxable_income_total_by_band__row_36", + "ird_taxable_income_total_by_band__row_37", + "ird_taxable_income_total_by_band__row_38", + "ird_taxable_income_total_by_band__row_39", + "ird_taxable_income_total_by_band__row_40", + "ird_taxable_income_total_by_band__row_41", + "ird_taxable_income_total_by_band__row_42", + "ird_taxable_income_total_by_band__row_43", + "ird_taxable_income_total_by_band__row_44", + "ird_taxable_income_total_by_band__row_45", + "ird_taxable_income_total_by_band__row_46", + "ird_taxable_income_total_by_band__row_47", + "ird_taxable_income_total_by_band__row_48", + "ird_taxable_income_total_by_band__row_49", + "ird_taxable_income_total_by_band__row_50", + "ird_taxable_income_total_by_band__row_51", + "ird_taxable_income_total_by_band__row_52", + "ird_taxable_income_total_by_band__row_53", + "ird_taxable_income_total_by_band__row_54", + "ird_taxable_income_total_by_band__row_55", + "ird_taxable_income_total_by_band__row_56", + "ird_taxable_income_total_by_band__row_57", + "ird_taxable_income_total_by_band__row_58", + "ird_taxable_income_total_by_band__row_59", + "ird_taxable_income_total_by_band__row_60", + "ird_taxable_income_total_by_band__row_61", + "ird_taxable_income_total_by_band__row_62", + "ird_taxable_income_total_by_band__row_63", + "ird_taxable_income_total_by_band__row_64", + "ird_taxable_income_total_by_band__row_65", + "ird_taxable_income_total_by_band__row_66", + "ird_taxable_income_total_by_band__row_67", + "ird_taxable_income_total_by_band__row_68", + "ird_taxable_income_total_by_band__row_69", + "ird_taxable_income_total_by_band__row_70", + "ird_taxable_income_total_by_band__row_71", + "ird_taxable_income_total_by_band__row_72", + "ird_taxable_income_total_by_band__row_73", + "ird_taxable_income_total_by_band__row_74", + "ird_taxable_income_total_by_band__row_75", + "ird_taxable_income_total_by_band__row_76", + "ird_taxable_income_total_by_band__row_77", + "ird_taxable_income_total_by_band__row_78", + "ird_taxable_income_total_by_band__row_79", + "ird_taxable_income_total_by_band__row_80", + "ird_taxable_income_total_by_band__row_81", + "ird_taxable_income_total_by_band__row_82", + "ird_taxable_income_total_by_band__row_83", + "ird_taxable_income_total_by_band__row_84", + "ird_taxable_income_total_by_band__row_85", + "ird_taxable_income_total_by_band__row_86", + "ird_taxable_income_total_by_band__row_87", + "ird_taxable_income_total_by_band__row_88", + "ird_taxable_income_total_by_band__row_89", + "ird_taxable_income_total_by_band__row_90", + "ird_taxable_income_total_by_band__row_91", + "ird_taxable_income_total_by_band__row_92", + "ird_taxable_income_total_by_band__row_93", + "ird_taxable_income_total_by_band__row_94", + "ird_taxable_income_total_by_band__row_95", + "ird_taxable_income_total_by_band__row_96", + "ird_taxable_income_total_by_band__row_97", + "ird_taxable_income_total_by_band__row_98", + "ird_taxable_income_total_by_band__row_99", + "ird_taxable_income_total_by_band__row_100", + "ird_taxable_income_total_by_band__row_101", + "ird_taxable_income_total_by_band__row_102", + "ird_taxable_income_total_by_band__row_103", + "ird_taxable_income_total_by_band__row_104", + "ird_taxable_income_total_by_band__row_105", + "ird_taxable_income_total_by_band__row_106", + "ird_taxable_income_total_by_band__row_107", + "ird_taxable_income_total_by_band__row_108", + "ird_taxable_income_total_by_band__row_109", + "ird_taxable_income_total_by_band__row_110", + "ird_taxable_income_total_by_band__row_111", + "ird_taxable_income_total_by_band__row_112", + "ird_taxable_income_total_by_band__row_113", + "ird_taxable_income_total_by_band__row_114", + "ird_taxable_income_total_by_band__row_115", + "ird_taxable_income_total_by_band__row_116", + "ird_taxable_income_total_by_band__row_117", + "ird_taxable_income_total_by_band__row_118", + "ird_taxable_income_total_by_band__row_119", + "ird_taxable_income_total_by_band__row_120", + "ird_taxable_income_total_by_band__row_121", + "ird_taxable_income_total_by_band__row_122", + "ird_taxable_income_total_by_band__row_123", + "ird_taxable_income_total_by_band__row_124", + "ird_taxable_income_total_by_band__row_125", + "ird_taxable_income_total_by_band__row_126", + "ird_taxable_income_total_by_band__row_127", + "ird_taxable_income_total_by_band__row_128", + "ird_taxable_income_total_by_band__row_129", + "ird_taxable_income_total_by_band__row_130", + "ird_taxable_income_total_by_band__row_131", + "ird_taxable_income_total_by_band__row_132", + "ird_taxable_income_total_by_band__row_133", + "ird_taxable_income_total_by_band__row_134", + "ird_taxable_income_total_by_band__row_135", + "ird_taxable_income_total_by_band__row_136", + "ird_taxable_income_total_by_band__row_137", + "ird_taxable_income_total_by_band__row_138", + "ird_taxable_income_total_by_band__row_139", + "ird_taxable_income_total_by_band__row_140", + "ird_taxable_income_total_by_band__row_141", + "ird_taxable_income_total_by_band__row_142", + "ird_taxable_income_total_by_band__row_143", + "ird_taxable_income_total_by_band__row_144", + "ird_taxable_income_total_by_band__row_145", + "ird_taxable_income_total_by_band__row_146", + "ird_taxable_income_total_by_band__row_147", + "ird_taxable_income_total_by_band__row_148", + "ird_taxable_income_total_by_band__row_149", + "ird_taxable_income_total_by_band__row_150", + "ird_taxable_income_total_by_band__row_151", + "ird_taxable_income_total_by_band__row_152", + "ird_taxable_income_total_by_band__row_153", + "ird_taxable_income_total_by_band__row_154", + "ird_taxable_income_total_by_band__row_155", + "ird_taxable_income_total_by_band__row_156", + "ird_taxable_income_total_by_band__row_157", + "ird_taxable_income_total_by_band__row_158", + "ird_taxable_income_total_by_band__row_159", + "ird_taxable_income_total_by_band__row_160", + "ird_taxable_income_total_by_band__row_161", + "ird_taxable_income_total_by_band__row_162", + "ird_taxable_income_total_by_band__row_163", + "ird_taxable_income_total_by_band__row_164", + "ird_taxable_income_total_by_band__row_165", + "ird_taxable_income_total_by_band__row_166", + "ird_taxable_income_total_by_band__row_167", + "ird_taxable_income_total_by_band__row_168", + "ird_taxable_income_total_by_band__row_169", + "ird_taxable_income_total_by_band__row_170", + "ird_taxable_income_total_by_band__row_171", + "ird_taxable_income_total_by_band__row_172", + "ird_taxable_income_total_by_band__row_173", + "ird_taxable_income_total_by_band__row_174", + "ird_taxable_income_total_by_band__row_175", + "ird_taxable_income_total_by_band__row_176", + "ird_taxable_income_total_by_band__row_177", + "ird_taxable_income_total_by_band__row_178", + "ird_taxable_income_total_by_band__row_179", + "ird_taxable_income_total_by_band__row_180", + "ird_taxable_income_total_by_band__row_181", + "ird_taxable_income_total_by_band__row_182", + "ird_taxable_income_total_by_band__row_183", + "ird_taxable_income_total_by_band__row_184", + "ird_taxable_income_total_by_band__row_185", + "ird_taxable_income_total_by_band__row_186", + "ird_taxable_income_total_by_band__row_187", + "ird_taxable_income_total_by_band__row_188", + "ird_taxable_income_total_by_band__row_189", + "ird_taxable_income_total_by_band__row_190", + "ird_taxable_income_total_by_band__row_191", + "ird_taxable_income_total_by_band__row_192", + "ird_taxable_income_total_by_band__row_193", + "ird_taxable_income_total_by_band__row_194", + "ird_taxable_income_total_by_band__row_195", + "ird_taxable_income_total_by_band__row_196", + "ird_taxable_income_total_by_band__row_197", + "ird_taxable_income_total_by_band__row_198", + "ird_taxable_income_total_by_band__row_199", + "ird_taxable_income_total_by_band__row_200", + "ird_taxable_income_total_by_band__row_201", + "ird_taxable_income_total_by_band__row_202", + "ird_taxable_income_total_by_band__row_203", + "ird_taxable_income_total_by_band__row_204", + "ird_taxable_income_total_by_band__row_205", + "ird_taxable_income_total_by_band__row_206", + "ird_taxable_income_total_by_band__row_207", + "ird_taxable_income_total_by_band__row_208", + "ird_taxable_income_total_by_band__row_209", + "ird_taxable_income_total_by_band__row_210", + "ird_taxable_income_total_by_band__row_211", + "ird_taxable_income_total_by_band__row_212", + "ird_taxable_income_total_by_band__row_213", + "ird_taxable_income_total_by_band__row_214", + "ird_taxable_income_total_by_band__row_215", + "ird_taxable_income_total_by_band__row_216", + "ird_taxable_income_total_by_band__row_217", + "ird_taxable_income_total_by_band__row_218", + "ird_taxable_income_total_by_band__row_219", + "ird_taxable_income_total_by_band__row_220", + "ird_taxable_income_total_by_band__row_221", + "ird_taxable_income_total_by_band__row_222", + "ird_taxable_income_total_by_band__row_223", + "ird_taxable_income_total_by_band__row_224", + "ird_taxable_income_total_by_band__row_225", + "ird_taxable_income_total_by_band__row_226", + "ird_taxable_income_total_by_band__row_227", + "ird_taxable_income_total_by_band__row_228", + "ird_taxable_income_total_by_band__row_229", + "ird_taxable_income_total_by_band__row_230", + "ird_taxable_income_total_by_band__row_231", + "ird_taxable_income_total_by_band__row_232", + "ird_taxable_income_total_by_band__row_233", + "ird_taxable_income_total_by_band__row_234", + "ird_taxable_income_total_by_band__row_235", + "ird_taxable_income_total_by_band__row_236", + "ird_taxable_income_total_by_band__row_237", + "ird_taxable_income_total_by_band__row_238", + "ird_taxable_income_total_by_band__row_239", + "ird_taxable_income_total_by_band__row_240", + "ird_taxable_income_total_by_band__row_241", + "ird_taxable_income_total_by_band__row_242", + "ird_taxable_income_total_by_band__row_243", + "ird_taxable_income_total_by_band__row_244" + ], + "count_reference_names": [ + "ird_taxable_income_people_by_band__row_8", + "ird_taxable_income_people_by_band__row_9", + "ird_taxable_income_people_by_band__row_10", + "ird_taxable_income_people_by_band__row_11", + "ird_taxable_income_people_by_band__row_12", + "ird_taxable_income_people_by_band__row_13", + "ird_taxable_income_people_by_band__row_14", + "ird_taxable_income_people_by_band__row_15", + "ird_taxable_income_people_by_band__row_16", + "ird_taxable_income_people_by_band__row_17", + "ird_taxable_income_people_by_band__row_18", + "ird_taxable_income_people_by_band__row_19", + "ird_taxable_income_people_by_band__row_20", + "ird_taxable_income_people_by_band__row_21", + "ird_taxable_income_people_by_band__row_22", + "ird_taxable_income_people_by_band__row_23", + "ird_taxable_income_people_by_band__row_24", + "ird_taxable_income_people_by_band__row_25", + "ird_taxable_income_people_by_band__row_26", + "ird_taxable_income_people_by_band__row_27", + "ird_taxable_income_people_by_band__row_28", + "ird_taxable_income_people_by_band__row_29", + "ird_taxable_income_people_by_band__row_30", + "ird_taxable_income_people_by_band__row_31", + "ird_taxable_income_people_by_band__row_32", + "ird_taxable_income_people_by_band__row_33", + "ird_taxable_income_people_by_band__row_34", + "ird_taxable_income_people_by_band__row_35", + "ird_taxable_income_people_by_band__row_36", + "ird_taxable_income_people_by_band__row_37", + "ird_taxable_income_people_by_band__row_38", + "ird_taxable_income_people_by_band__row_39", + "ird_taxable_income_people_by_band__row_40", + "ird_taxable_income_people_by_band__row_41", + "ird_taxable_income_people_by_band__row_42", + "ird_taxable_income_people_by_band__row_43", + "ird_taxable_income_people_by_band__row_44", + "ird_taxable_income_people_by_band__row_45", + "ird_taxable_income_people_by_band__row_46", + "ird_taxable_income_people_by_band__row_47", + "ird_taxable_income_people_by_band__row_48", + "ird_taxable_income_people_by_band__row_49", + "ird_taxable_income_people_by_band__row_50", + "ird_taxable_income_people_by_band__row_51", + "ird_taxable_income_people_by_band__row_52", + "ird_taxable_income_people_by_band__row_53", + "ird_taxable_income_people_by_band__row_54", + "ird_taxable_income_people_by_band__row_55", + "ird_taxable_income_people_by_band__row_56", + "ird_taxable_income_people_by_band__row_57", + "ird_taxable_income_people_by_band__row_58", + "ird_taxable_income_people_by_band__row_59", + "ird_taxable_income_people_by_band__row_60", + "ird_taxable_income_people_by_band__row_61", + "ird_taxable_income_people_by_band__row_62", + "ird_taxable_income_people_by_band__row_63", + "ird_taxable_income_people_by_band__row_64", + "ird_taxable_income_people_by_band__row_65", + "ird_taxable_income_people_by_band__row_66", + "ird_taxable_income_people_by_band__row_67", + "ird_taxable_income_people_by_band__row_68", + "ird_taxable_income_people_by_band__row_69", + "ird_taxable_income_people_by_band__row_70", + "ird_taxable_income_people_by_band__row_71", + "ird_taxable_income_people_by_band__row_72", + "ird_taxable_income_people_by_band__row_73", + "ird_taxable_income_people_by_band__row_74", + "ird_taxable_income_people_by_band__row_75", + "ird_taxable_income_people_by_band__row_76", + "ird_taxable_income_people_by_band__row_77", + "ird_taxable_income_people_by_band__row_78", + "ird_taxable_income_people_by_band__row_79", + "ird_taxable_income_people_by_band__row_80", + "ird_taxable_income_people_by_band__row_81", + "ird_taxable_income_people_by_band__row_82", + "ird_taxable_income_people_by_band__row_83", + "ird_taxable_income_people_by_band__row_84", + "ird_taxable_income_people_by_band__row_85", + "ird_taxable_income_people_by_band__row_86", + "ird_taxable_income_people_by_band__row_87", + "ird_taxable_income_people_by_band__row_88", + "ird_taxable_income_people_by_band__row_89", + "ird_taxable_income_people_by_band__row_90", + "ird_taxable_income_people_by_band__row_91", + "ird_taxable_income_people_by_band__row_92", + "ird_taxable_income_people_by_band__row_93", + "ird_taxable_income_people_by_band__row_94", + "ird_taxable_income_people_by_band__row_95", + "ird_taxable_income_people_by_band__row_96", + "ird_taxable_income_people_by_band__row_97", + "ird_taxable_income_people_by_band__row_98", + "ird_taxable_income_people_by_band__row_99", + "ird_taxable_income_people_by_band__row_100", + "ird_taxable_income_people_by_band__row_101", + "ird_taxable_income_people_by_band__row_102", + "ird_taxable_income_people_by_band__row_103", + "ird_taxable_income_people_by_band__row_104", + "ird_taxable_income_people_by_band__row_105", + "ird_taxable_income_people_by_band__row_106", + "ird_taxable_income_people_by_band__row_107", + "ird_taxable_income_people_by_band__row_108", + "ird_taxable_income_people_by_band__row_109", + "ird_taxable_income_people_by_band__row_110", + "ird_taxable_income_people_by_band__row_111", + "ird_taxable_income_people_by_band__row_112", + "ird_taxable_income_people_by_band__row_113", + "ird_taxable_income_people_by_band__row_114", + "ird_taxable_income_people_by_band__row_115", + "ird_taxable_income_people_by_band__row_116", + "ird_taxable_income_people_by_band__row_117", + "ird_taxable_income_people_by_band__row_118", + "ird_taxable_income_people_by_band__row_119", + "ird_taxable_income_people_by_band__row_120", + "ird_taxable_income_people_by_band__row_121", + "ird_taxable_income_people_by_band__row_122", + "ird_taxable_income_people_by_band__row_123", + "ird_taxable_income_people_by_band__row_124", + "ird_taxable_income_people_by_band__row_125", + "ird_taxable_income_people_by_band__row_126", + "ird_taxable_income_people_by_band__row_127", + "ird_taxable_income_people_by_band__row_128", + "ird_taxable_income_people_by_band__row_129", + "ird_taxable_income_people_by_band__row_130", + "ird_taxable_income_people_by_band__row_131", + "ird_taxable_income_people_by_band__row_132", + "ird_taxable_income_people_by_band__row_133", + "ird_taxable_income_people_by_band__row_134", + "ird_taxable_income_people_by_band__row_135", + "ird_taxable_income_people_by_band__row_136", + "ird_taxable_income_people_by_band__row_137", + "ird_taxable_income_people_by_band__row_138", + "ird_taxable_income_people_by_band__row_139", + "ird_taxable_income_people_by_band__row_140", + "ird_taxable_income_people_by_band__row_141", + "ird_taxable_income_people_by_band__row_142", + "ird_taxable_income_people_by_band__row_143", + "ird_taxable_income_people_by_band__row_144", + "ird_taxable_income_people_by_band__row_145", + "ird_taxable_income_people_by_band__row_146", + "ird_taxable_income_people_by_band__row_147", + "ird_taxable_income_people_by_band__row_148", + "ird_taxable_income_people_by_band__row_149", + "ird_taxable_income_people_by_band__row_150", + "ird_taxable_income_people_by_band__row_151", + "ird_taxable_income_people_by_band__row_152", + "ird_taxable_income_people_by_band__row_153", + "ird_taxable_income_people_by_band__row_154", + "ird_taxable_income_people_by_band__row_155", + "ird_taxable_income_people_by_band__row_156", + "ird_taxable_income_people_by_band__row_157", + "ird_taxable_income_people_by_band__row_158", + "ird_taxable_income_people_by_band__row_159", + "ird_taxable_income_people_by_band__row_160", + "ird_taxable_income_people_by_band__row_161", + "ird_taxable_income_people_by_band__row_162", + "ird_taxable_income_people_by_band__row_163", + "ird_taxable_income_people_by_band__row_164", + "ird_taxable_income_people_by_band__row_165", + "ird_taxable_income_people_by_band__row_166", + "ird_taxable_income_people_by_band__row_167", + "ird_taxable_income_people_by_band__row_168", + "ird_taxable_income_people_by_band__row_169", + "ird_taxable_income_people_by_band__row_170", + "ird_taxable_income_people_by_band__row_171", + "ird_taxable_income_people_by_band__row_172", + "ird_taxable_income_people_by_band__row_173", + "ird_taxable_income_people_by_band__row_174", + "ird_taxable_income_people_by_band__row_175", + "ird_taxable_income_people_by_band__row_176", + "ird_taxable_income_people_by_band__row_177", + "ird_taxable_income_people_by_band__row_178", + "ird_taxable_income_people_by_band__row_179", + "ird_taxable_income_people_by_band__row_180", + "ird_taxable_income_people_by_band__row_181", + "ird_taxable_income_people_by_band__row_182", + "ird_taxable_income_people_by_band__row_183", + "ird_taxable_income_people_by_band__row_184", + "ird_taxable_income_people_by_band__row_185", + "ird_taxable_income_people_by_band__row_186", + "ird_taxable_income_people_by_band__row_187", + "ird_taxable_income_people_by_band__row_188", + "ird_taxable_income_people_by_band__row_189", + "ird_taxable_income_people_by_band__row_190", + "ird_taxable_income_people_by_band__row_191", + "ird_taxable_income_people_by_band__row_192", + "ird_taxable_income_people_by_band__row_193", + "ird_taxable_income_people_by_band__row_194", + "ird_taxable_income_people_by_band__row_195", + "ird_taxable_income_people_by_band__row_196", + "ird_taxable_income_people_by_band__row_197", + "ird_taxable_income_people_by_band__row_198", + "ird_taxable_income_people_by_band__row_199", + "ird_taxable_income_people_by_band__row_200", + "ird_taxable_income_people_by_band__row_201", + "ird_taxable_income_people_by_band__row_202", + "ird_taxable_income_people_by_band__row_203", + "ird_taxable_income_people_by_band__row_204", + "ird_taxable_income_people_by_band__row_205", + "ird_taxable_income_people_by_band__row_206", + "ird_taxable_income_people_by_band__row_207", + "ird_taxable_income_people_by_band__row_208", + "ird_taxable_income_people_by_band__row_209", + "ird_taxable_income_people_by_band__row_210", + "ird_taxable_income_people_by_band__row_211", + "ird_taxable_income_people_by_band__row_212", + "ird_taxable_income_people_by_band__row_213", + "ird_taxable_income_people_by_band__row_214", + "ird_taxable_income_people_by_band__row_215", + "ird_taxable_income_people_by_band__row_216", + "ird_taxable_income_people_by_band__row_217", + "ird_taxable_income_people_by_band__row_218", + "ird_taxable_income_people_by_band__row_219", + "ird_taxable_income_people_by_band__row_220", + "ird_taxable_income_people_by_band__row_221", + "ird_taxable_income_people_by_band__row_222", + "ird_taxable_income_people_by_band__row_223", + "ird_taxable_income_people_by_band__row_224", + "ird_taxable_income_people_by_band__row_225", + "ird_taxable_income_people_by_band__row_226", + "ird_taxable_income_people_by_band__row_227", + "ird_taxable_income_people_by_band__row_228", + "ird_taxable_income_people_by_band__row_229", + "ird_taxable_income_people_by_band__row_230", + "ird_taxable_income_people_by_band__row_231", + "ird_taxable_income_people_by_band__row_232", + "ird_taxable_income_people_by_band__row_233", + "ird_taxable_income_people_by_band__row_234", + "ird_taxable_income_people_by_band__row_235", + "ird_taxable_income_people_by_band__row_236", + "ird_taxable_income_people_by_band__row_237", + "ird_taxable_income_people_by_band__row_238", + "ird_taxable_income_people_by_band__row_239", + "ird_taxable_income_people_by_band__row_240", + "ird_taxable_income_people_by_band__row_241", + "ird_taxable_income_people_by_band__row_242", + "ird_taxable_income_people_by_band__row_243", + "ird_taxable_income_people_by_band__row_244" + ], + "interval_table_ids": [], + "required_measurement_binding": "ird_taxable_income_ty2027_v1" + } + ] + }, + "central": { + "transformations": [ + { + "operation": "scale_money_and_intervals", + "factor_id": "ird_ty2024_to_ty2027_central", + "reference_names": [ + "ird_taxable_income_total_by_band__row_8", + "ird_taxable_income_total_by_band__row_9", + "ird_taxable_income_total_by_band__row_10", + "ird_taxable_income_total_by_band__row_11", + "ird_taxable_income_total_by_band__row_12", + "ird_taxable_income_total_by_band__row_13", + "ird_taxable_income_total_by_band__row_14", + "ird_taxable_income_total_by_band__row_15", + "ird_taxable_income_total_by_band__row_16", + "ird_taxable_income_total_by_band__row_17", + "ird_taxable_income_total_by_band__row_18", + "ird_taxable_income_total_by_band__row_19", + "ird_taxable_income_total_by_band__row_20", + "ird_taxable_income_total_by_band__row_21", + "ird_taxable_income_total_by_band__row_22", + "ird_taxable_income_total_by_band__row_23", + "ird_taxable_income_total_by_band__row_24", + "ird_taxable_income_total_by_band__row_25", + "ird_taxable_income_total_by_band__row_26", + "ird_taxable_income_total_by_band__row_27", + "ird_taxable_income_total_by_band__row_28", + "ird_taxable_income_total_by_band__row_29", + "ird_taxable_income_total_by_band__row_30", + "ird_taxable_income_total_by_band__row_31", + "ird_taxable_income_total_by_band__row_32", + "ird_taxable_income_total_by_band__row_33", + "ird_taxable_income_total_by_band__row_34", + "ird_taxable_income_total_by_band__row_35", + "ird_taxable_income_total_by_band__row_36", + "ird_taxable_income_total_by_band__row_37", + "ird_taxable_income_total_by_band__row_38", + "ird_taxable_income_total_by_band__row_39", + "ird_taxable_income_total_by_band__row_40", + "ird_taxable_income_total_by_band__row_41", + "ird_taxable_income_total_by_band__row_42", + "ird_taxable_income_total_by_band__row_43", + "ird_taxable_income_total_by_band__row_44", + "ird_taxable_income_total_by_band__row_45", + "ird_taxable_income_total_by_band__row_46", + "ird_taxable_income_total_by_band__row_47", + "ird_taxable_income_total_by_band__row_48", + "ird_taxable_income_total_by_band__row_49", + "ird_taxable_income_total_by_band__row_50", + "ird_taxable_income_total_by_band__row_51", + "ird_taxable_income_total_by_band__row_52", + "ird_taxable_income_total_by_band__row_53", + "ird_taxable_income_total_by_band__row_54", + "ird_taxable_income_total_by_band__row_55", + "ird_taxable_income_total_by_band__row_56", + "ird_taxable_income_total_by_band__row_57", + "ird_taxable_income_total_by_band__row_58", + "ird_taxable_income_total_by_band__row_59", + "ird_taxable_income_total_by_band__row_60", + "ird_taxable_income_total_by_band__row_61", + "ird_taxable_income_total_by_band__row_62", + "ird_taxable_income_total_by_band__row_63", + "ird_taxable_income_total_by_band__row_64", + "ird_taxable_income_total_by_band__row_65", + "ird_taxable_income_total_by_band__row_66", + "ird_taxable_income_total_by_band__row_67", + "ird_taxable_income_total_by_band__row_68", + "ird_taxable_income_total_by_band__row_69", + "ird_taxable_income_total_by_band__row_70", + "ird_taxable_income_total_by_band__row_71", + "ird_taxable_income_total_by_band__row_72", + "ird_taxable_income_total_by_band__row_73", + "ird_taxable_income_total_by_band__row_74", + "ird_taxable_income_total_by_band__row_75", + "ird_taxable_income_total_by_band__row_76", + "ird_taxable_income_total_by_band__row_77", + "ird_taxable_income_total_by_band__row_78", + "ird_taxable_income_total_by_band__row_79", + "ird_taxable_income_total_by_band__row_80", + "ird_taxable_income_total_by_band__row_81", + "ird_taxable_income_total_by_band__row_82", + "ird_taxable_income_total_by_band__row_83", + "ird_taxable_income_total_by_band__row_84", + "ird_taxable_income_total_by_band__row_85", + "ird_taxable_income_total_by_band__row_86", + "ird_taxable_income_total_by_band__row_87", + "ird_taxable_income_total_by_band__row_88", + "ird_taxable_income_total_by_band__row_89", + "ird_taxable_income_total_by_band__row_90", + "ird_taxable_income_total_by_band__row_91", + "ird_taxable_income_total_by_band__row_92", + "ird_taxable_income_total_by_band__row_93", + "ird_taxable_income_total_by_band__row_94", + "ird_taxable_income_total_by_band__row_95", + "ird_taxable_income_total_by_band__row_96", + "ird_taxable_income_total_by_band__row_97", + "ird_taxable_income_total_by_band__row_98", + "ird_taxable_income_total_by_band__row_99", + "ird_taxable_income_total_by_band__row_100", + "ird_taxable_income_total_by_band__row_101", + "ird_taxable_income_total_by_band__row_102", + "ird_taxable_income_total_by_band__row_103", + "ird_taxable_income_total_by_band__row_104", + "ird_taxable_income_total_by_band__row_105", + "ird_taxable_income_total_by_band__row_106", + "ird_taxable_income_total_by_band__row_107", + "ird_taxable_income_total_by_band__row_108", + "ird_taxable_income_total_by_band__row_109", + "ird_taxable_income_total_by_band__row_110", + "ird_taxable_income_total_by_band__row_111", + "ird_taxable_income_total_by_band__row_112", + "ird_taxable_income_total_by_band__row_113", + "ird_taxable_income_total_by_band__row_114", + "ird_taxable_income_total_by_band__row_115", + "ird_taxable_income_total_by_band__row_116", + "ird_taxable_income_total_by_band__row_117", + "ird_taxable_income_total_by_band__row_118", + "ird_taxable_income_total_by_band__row_119", + "ird_taxable_income_total_by_band__row_120", + "ird_taxable_income_total_by_band__row_121", + "ird_taxable_income_total_by_band__row_122", + "ird_taxable_income_total_by_band__row_123", + "ird_taxable_income_total_by_band__row_124", + "ird_taxable_income_total_by_band__row_125", + "ird_taxable_income_total_by_band__row_126", + "ird_taxable_income_total_by_band__row_127", + "ird_taxable_income_total_by_band__row_128", + "ird_taxable_income_total_by_band__row_129", + "ird_taxable_income_total_by_band__row_130", + "ird_taxable_income_total_by_band__row_131", + "ird_taxable_income_total_by_band__row_132", + "ird_taxable_income_total_by_band__row_133", + "ird_taxable_income_total_by_band__row_134", + "ird_taxable_income_total_by_band__row_135", + "ird_taxable_income_total_by_band__row_136", + "ird_taxable_income_total_by_band__row_137", + "ird_taxable_income_total_by_band__row_138", + "ird_taxable_income_total_by_band__row_139", + "ird_taxable_income_total_by_band__row_140", + "ird_taxable_income_total_by_band__row_141", + "ird_taxable_income_total_by_band__row_142", + "ird_taxable_income_total_by_band__row_143", + "ird_taxable_income_total_by_band__row_144", + "ird_taxable_income_total_by_band__row_145", + "ird_taxable_income_total_by_band__row_146", + "ird_taxable_income_total_by_band__row_147", + "ird_taxable_income_total_by_band__row_148", + "ird_taxable_income_total_by_band__row_149", + "ird_taxable_income_total_by_band__row_150", + "ird_taxable_income_total_by_band__row_151", + "ird_taxable_income_total_by_band__row_152", + "ird_taxable_income_total_by_band__row_153", + "ird_taxable_income_total_by_band__row_154", + "ird_taxable_income_total_by_band__row_155", + "ird_taxable_income_total_by_band__row_156", + "ird_taxable_income_total_by_band__row_157", + "ird_taxable_income_total_by_band__row_158", + "ird_taxable_income_total_by_band__row_159", + "ird_taxable_income_total_by_band__row_160", + "ird_taxable_income_total_by_band__row_161", + "ird_taxable_income_total_by_band__row_162", + "ird_taxable_income_total_by_band__row_163", + "ird_taxable_income_total_by_band__row_164", + "ird_taxable_income_total_by_band__row_165", + "ird_taxable_income_total_by_band__row_166", + "ird_taxable_income_total_by_band__row_167", + "ird_taxable_income_total_by_band__row_168", + "ird_taxable_income_total_by_band__row_169", + "ird_taxable_income_total_by_band__row_170", + "ird_taxable_income_total_by_band__row_171", + "ird_taxable_income_total_by_band__row_172", + "ird_taxable_income_total_by_band__row_173", + "ird_taxable_income_total_by_band__row_174", + "ird_taxable_income_total_by_band__row_175", + "ird_taxable_income_total_by_band__row_176", + "ird_taxable_income_total_by_band__row_177", + "ird_taxable_income_total_by_band__row_178", + "ird_taxable_income_total_by_band__row_179", + "ird_taxable_income_total_by_band__row_180", + "ird_taxable_income_total_by_band__row_181", + "ird_taxable_income_total_by_band__row_182", + "ird_taxable_income_total_by_band__row_183", + "ird_taxable_income_total_by_band__row_184", + "ird_taxable_income_total_by_band__row_185", + "ird_taxable_income_total_by_band__row_186", + "ird_taxable_income_total_by_band__row_187", + "ird_taxable_income_total_by_band__row_188", + "ird_taxable_income_total_by_band__row_189", + "ird_taxable_income_total_by_band__row_190", + "ird_taxable_income_total_by_band__row_191", + "ird_taxable_income_total_by_band__row_192", + "ird_taxable_income_total_by_band__row_193", + "ird_taxable_income_total_by_band__row_194", + "ird_taxable_income_total_by_band__row_195", + "ird_taxable_income_total_by_band__row_196", + "ird_taxable_income_total_by_band__row_197", + "ird_taxable_income_total_by_band__row_198", + "ird_taxable_income_total_by_band__row_199", + "ird_taxable_income_total_by_band__row_200", + "ird_taxable_income_total_by_band__row_201", + "ird_taxable_income_total_by_band__row_202", + "ird_taxable_income_total_by_band__row_203", + "ird_taxable_income_total_by_band__row_204", + "ird_taxable_income_total_by_band__row_205", + "ird_taxable_income_total_by_band__row_206", + "ird_taxable_income_total_by_band__row_207", + "ird_taxable_income_total_by_band__row_208", + "ird_taxable_income_total_by_band__row_209", + "ird_taxable_income_total_by_band__row_210", + "ird_taxable_income_total_by_band__row_211", + "ird_taxable_income_total_by_band__row_212", + "ird_taxable_income_total_by_band__row_213", + "ird_taxable_income_total_by_band__row_214", + "ird_taxable_income_total_by_band__row_215", + "ird_taxable_income_total_by_band__row_216", + "ird_taxable_income_total_by_band__row_217", + "ird_taxable_income_total_by_band__row_218", + "ird_taxable_income_total_by_band__row_219", + "ird_taxable_income_total_by_band__row_220", + "ird_taxable_income_total_by_band__row_221", + "ird_taxable_income_total_by_band__row_222", + "ird_taxable_income_total_by_band__row_223", + "ird_taxable_income_total_by_band__row_224", + "ird_taxable_income_total_by_band__row_225", + "ird_taxable_income_total_by_band__row_226", + "ird_taxable_income_total_by_band__row_227", + "ird_taxable_income_total_by_band__row_228", + "ird_taxable_income_total_by_band__row_229", + "ird_taxable_income_total_by_band__row_230", + "ird_taxable_income_total_by_band__row_231", + "ird_taxable_income_total_by_band__row_232", + "ird_taxable_income_total_by_band__row_233", + "ird_taxable_income_total_by_band__row_234", + "ird_taxable_income_total_by_band__row_235", + "ird_taxable_income_total_by_band__row_236", + "ird_taxable_income_total_by_band__row_237", + "ird_taxable_income_total_by_band__row_238", + "ird_taxable_income_total_by_band__row_239", + "ird_taxable_income_total_by_band__row_240", + "ird_taxable_income_total_by_band__row_241", + "ird_taxable_income_total_by_band__row_242", + "ird_taxable_income_total_by_band__row_243", + "ird_taxable_income_total_by_band__row_244" + ], + "count_reference_names": [ + "ird_taxable_income_people_by_band__row_8", + "ird_taxable_income_people_by_band__row_9", + "ird_taxable_income_people_by_band__row_10", + "ird_taxable_income_people_by_band__row_11", + "ird_taxable_income_people_by_band__row_12", + "ird_taxable_income_people_by_band__row_13", + "ird_taxable_income_people_by_band__row_14", + "ird_taxable_income_people_by_band__row_15", + "ird_taxable_income_people_by_band__row_16", + "ird_taxable_income_people_by_band__row_17", + "ird_taxable_income_people_by_band__row_18", + "ird_taxable_income_people_by_band__row_19", + "ird_taxable_income_people_by_band__row_20", + "ird_taxable_income_people_by_band__row_21", + "ird_taxable_income_people_by_band__row_22", + "ird_taxable_income_people_by_band__row_23", + "ird_taxable_income_people_by_band__row_24", + "ird_taxable_income_people_by_band__row_25", + "ird_taxable_income_people_by_band__row_26", + "ird_taxable_income_people_by_band__row_27", + "ird_taxable_income_people_by_band__row_28", + "ird_taxable_income_people_by_band__row_29", + "ird_taxable_income_people_by_band__row_30", + "ird_taxable_income_people_by_band__row_31", + "ird_taxable_income_people_by_band__row_32", + "ird_taxable_income_people_by_band__row_33", + "ird_taxable_income_people_by_band__row_34", + "ird_taxable_income_people_by_band__row_35", + "ird_taxable_income_people_by_band__row_36", + "ird_taxable_income_people_by_band__row_37", + "ird_taxable_income_people_by_band__row_38", + "ird_taxable_income_people_by_band__row_39", + "ird_taxable_income_people_by_band__row_40", + "ird_taxable_income_people_by_band__row_41", + "ird_taxable_income_people_by_band__row_42", + "ird_taxable_income_people_by_band__row_43", + "ird_taxable_income_people_by_band__row_44", + "ird_taxable_income_people_by_band__row_45", + "ird_taxable_income_people_by_band__row_46", + "ird_taxable_income_people_by_band__row_47", + "ird_taxable_income_people_by_band__row_48", + "ird_taxable_income_people_by_band__row_49", + "ird_taxable_income_people_by_band__row_50", + "ird_taxable_income_people_by_band__row_51", + "ird_taxable_income_people_by_band__row_52", + "ird_taxable_income_people_by_band__row_53", + "ird_taxable_income_people_by_band__row_54", + "ird_taxable_income_people_by_band__row_55", + "ird_taxable_income_people_by_band__row_56", + "ird_taxable_income_people_by_band__row_57", + "ird_taxable_income_people_by_band__row_58", + "ird_taxable_income_people_by_band__row_59", + "ird_taxable_income_people_by_band__row_60", + "ird_taxable_income_people_by_band__row_61", + "ird_taxable_income_people_by_band__row_62", + "ird_taxable_income_people_by_band__row_63", + "ird_taxable_income_people_by_band__row_64", + "ird_taxable_income_people_by_band__row_65", + "ird_taxable_income_people_by_band__row_66", + "ird_taxable_income_people_by_band__row_67", + "ird_taxable_income_people_by_band__row_68", + "ird_taxable_income_people_by_band__row_69", + "ird_taxable_income_people_by_band__row_70", + "ird_taxable_income_people_by_band__row_71", + "ird_taxable_income_people_by_band__row_72", + "ird_taxable_income_people_by_band__row_73", + "ird_taxable_income_people_by_band__row_74", + "ird_taxable_income_people_by_band__row_75", + "ird_taxable_income_people_by_band__row_76", + "ird_taxable_income_people_by_band__row_77", + "ird_taxable_income_people_by_band__row_78", + "ird_taxable_income_people_by_band__row_79", + "ird_taxable_income_people_by_band__row_80", + "ird_taxable_income_people_by_band__row_81", + "ird_taxable_income_people_by_band__row_82", + "ird_taxable_income_people_by_band__row_83", + "ird_taxable_income_people_by_band__row_84", + "ird_taxable_income_people_by_band__row_85", + "ird_taxable_income_people_by_band__row_86", + "ird_taxable_income_people_by_band__row_87", + "ird_taxable_income_people_by_band__row_88", + "ird_taxable_income_people_by_band__row_89", + "ird_taxable_income_people_by_band__row_90", + "ird_taxable_income_people_by_band__row_91", + "ird_taxable_income_people_by_band__row_92", + "ird_taxable_income_people_by_band__row_93", + "ird_taxable_income_people_by_band__row_94", + "ird_taxable_income_people_by_band__row_95", + "ird_taxable_income_people_by_band__row_96", + "ird_taxable_income_people_by_band__row_97", + "ird_taxable_income_people_by_band__row_98", + "ird_taxable_income_people_by_band__row_99", + "ird_taxable_income_people_by_band__row_100", + "ird_taxable_income_people_by_band__row_101", + "ird_taxable_income_people_by_band__row_102", + "ird_taxable_income_people_by_band__row_103", + "ird_taxable_income_people_by_band__row_104", + "ird_taxable_income_people_by_band__row_105", + "ird_taxable_income_people_by_band__row_106", + "ird_taxable_income_people_by_band__row_107", + "ird_taxable_income_people_by_band__row_108", + "ird_taxable_income_people_by_band__row_109", + "ird_taxable_income_people_by_band__row_110", + "ird_taxable_income_people_by_band__row_111", + "ird_taxable_income_people_by_band__row_112", + "ird_taxable_income_people_by_band__row_113", + "ird_taxable_income_people_by_band__row_114", + "ird_taxable_income_people_by_band__row_115", + "ird_taxable_income_people_by_band__row_116", + "ird_taxable_income_people_by_band__row_117", + "ird_taxable_income_people_by_band__row_118", + "ird_taxable_income_people_by_band__row_119", + "ird_taxable_income_people_by_band__row_120", + "ird_taxable_income_people_by_band__row_121", + "ird_taxable_income_people_by_band__row_122", + "ird_taxable_income_people_by_band__row_123", + "ird_taxable_income_people_by_band__row_124", + "ird_taxable_income_people_by_band__row_125", + "ird_taxable_income_people_by_band__row_126", + "ird_taxable_income_people_by_band__row_127", + "ird_taxable_income_people_by_band__row_128", + "ird_taxable_income_people_by_band__row_129", + "ird_taxable_income_people_by_band__row_130", + "ird_taxable_income_people_by_band__row_131", + "ird_taxable_income_people_by_band__row_132", + "ird_taxable_income_people_by_band__row_133", + "ird_taxable_income_people_by_band__row_134", + "ird_taxable_income_people_by_band__row_135", + "ird_taxable_income_people_by_band__row_136", + "ird_taxable_income_people_by_band__row_137", + "ird_taxable_income_people_by_band__row_138", + "ird_taxable_income_people_by_band__row_139", + "ird_taxable_income_people_by_band__row_140", + "ird_taxable_income_people_by_band__row_141", + "ird_taxable_income_people_by_band__row_142", + "ird_taxable_income_people_by_band__row_143", + "ird_taxable_income_people_by_band__row_144", + "ird_taxable_income_people_by_band__row_145", + "ird_taxable_income_people_by_band__row_146", + "ird_taxable_income_people_by_band__row_147", + "ird_taxable_income_people_by_band__row_148", + "ird_taxable_income_people_by_band__row_149", + "ird_taxable_income_people_by_band__row_150", + "ird_taxable_income_people_by_band__row_151", + "ird_taxable_income_people_by_band__row_152", + "ird_taxable_income_people_by_band__row_153", + "ird_taxable_income_people_by_band__row_154", + "ird_taxable_income_people_by_band__row_155", + "ird_taxable_income_people_by_band__row_156", + "ird_taxable_income_people_by_band__row_157", + "ird_taxable_income_people_by_band__row_158", + "ird_taxable_income_people_by_band__row_159", + "ird_taxable_income_people_by_band__row_160", + "ird_taxable_income_people_by_band__row_161", + "ird_taxable_income_people_by_band__row_162", + "ird_taxable_income_people_by_band__row_163", + "ird_taxable_income_people_by_band__row_164", + "ird_taxable_income_people_by_band__row_165", + "ird_taxable_income_people_by_band__row_166", + "ird_taxable_income_people_by_band__row_167", + "ird_taxable_income_people_by_band__row_168", + "ird_taxable_income_people_by_band__row_169", + "ird_taxable_income_people_by_band__row_170", + "ird_taxable_income_people_by_band__row_171", + "ird_taxable_income_people_by_band__row_172", + "ird_taxable_income_people_by_band__row_173", + "ird_taxable_income_people_by_band__row_174", + "ird_taxable_income_people_by_band__row_175", + "ird_taxable_income_people_by_band__row_176", + "ird_taxable_income_people_by_band__row_177", + "ird_taxable_income_people_by_band__row_178", + "ird_taxable_income_people_by_band__row_179", + "ird_taxable_income_people_by_band__row_180", + "ird_taxable_income_people_by_band__row_181", + "ird_taxable_income_people_by_band__row_182", + "ird_taxable_income_people_by_band__row_183", + "ird_taxable_income_people_by_band__row_184", + "ird_taxable_income_people_by_band__row_185", + "ird_taxable_income_people_by_band__row_186", + "ird_taxable_income_people_by_band__row_187", + "ird_taxable_income_people_by_band__row_188", + "ird_taxable_income_people_by_band__row_189", + "ird_taxable_income_people_by_band__row_190", + "ird_taxable_income_people_by_band__row_191", + "ird_taxable_income_people_by_band__row_192", + "ird_taxable_income_people_by_band__row_193", + "ird_taxable_income_people_by_band__row_194", + "ird_taxable_income_people_by_band__row_195", + "ird_taxable_income_people_by_band__row_196", + "ird_taxable_income_people_by_band__row_197", + "ird_taxable_income_people_by_band__row_198", + "ird_taxable_income_people_by_band__row_199", + "ird_taxable_income_people_by_band__row_200", + "ird_taxable_income_people_by_band__row_201", + "ird_taxable_income_people_by_band__row_202", + "ird_taxable_income_people_by_band__row_203", + "ird_taxable_income_people_by_band__row_204", + "ird_taxable_income_people_by_band__row_205", + "ird_taxable_income_people_by_band__row_206", + "ird_taxable_income_people_by_band__row_207", + "ird_taxable_income_people_by_band__row_208", + "ird_taxable_income_people_by_band__row_209", + "ird_taxable_income_people_by_band__row_210", + "ird_taxable_income_people_by_band__row_211", + "ird_taxable_income_people_by_band__row_212", + "ird_taxable_income_people_by_band__row_213", + "ird_taxable_income_people_by_band__row_214", + "ird_taxable_income_people_by_band__row_215", + "ird_taxable_income_people_by_band__row_216", + "ird_taxable_income_people_by_band__row_217", + "ird_taxable_income_people_by_band__row_218", + "ird_taxable_income_people_by_band__row_219", + "ird_taxable_income_people_by_band__row_220", + "ird_taxable_income_people_by_band__row_221", + "ird_taxable_income_people_by_band__row_222", + "ird_taxable_income_people_by_band__row_223", + "ird_taxable_income_people_by_band__row_224", + "ird_taxable_income_people_by_band__row_225", + "ird_taxable_income_people_by_band__row_226", + "ird_taxable_income_people_by_band__row_227", + "ird_taxable_income_people_by_band__row_228", + "ird_taxable_income_people_by_band__row_229", + "ird_taxable_income_people_by_band__row_230", + "ird_taxable_income_people_by_band__row_231", + "ird_taxable_income_people_by_band__row_232", + "ird_taxable_income_people_by_band__row_233", + "ird_taxable_income_people_by_band__row_234", + "ird_taxable_income_people_by_band__row_235", + "ird_taxable_income_people_by_band__row_236", + "ird_taxable_income_people_by_band__row_237", + "ird_taxable_income_people_by_band__row_238", + "ird_taxable_income_people_by_band__row_239", + "ird_taxable_income_people_by_band__row_240", + "ird_taxable_income_people_by_band__row_241", + "ird_taxable_income_people_by_band__row_242", + "ird_taxable_income_people_by_band__row_243", + "ird_taxable_income_people_by_band__row_244" + ], + "interval_table_ids": [], + "required_measurement_binding": "ird_taxable_income_ty2027_v1" + } + ] }, "high": { - "available": false, - "transformations": [] + "transformations": [ + { + "operation": "scale_money_and_intervals", + "factor_id": "ird_ty2024_to_ty2027_high", + "reference_names": [ + "ird_taxable_income_total_by_band__row_8", + "ird_taxable_income_total_by_band__row_9", + "ird_taxable_income_total_by_band__row_10", + "ird_taxable_income_total_by_band__row_11", + "ird_taxable_income_total_by_band__row_12", + "ird_taxable_income_total_by_band__row_13", + "ird_taxable_income_total_by_band__row_14", + "ird_taxable_income_total_by_band__row_15", + "ird_taxable_income_total_by_band__row_16", + "ird_taxable_income_total_by_band__row_17", + "ird_taxable_income_total_by_band__row_18", + "ird_taxable_income_total_by_band__row_19", + "ird_taxable_income_total_by_band__row_20", + "ird_taxable_income_total_by_band__row_21", + "ird_taxable_income_total_by_band__row_22", + "ird_taxable_income_total_by_band__row_23", + "ird_taxable_income_total_by_band__row_24", + "ird_taxable_income_total_by_band__row_25", + "ird_taxable_income_total_by_band__row_26", + "ird_taxable_income_total_by_band__row_27", + "ird_taxable_income_total_by_band__row_28", + "ird_taxable_income_total_by_band__row_29", + "ird_taxable_income_total_by_band__row_30", + "ird_taxable_income_total_by_band__row_31", + "ird_taxable_income_total_by_band__row_32", + "ird_taxable_income_total_by_band__row_33", + "ird_taxable_income_total_by_band__row_34", + "ird_taxable_income_total_by_band__row_35", + "ird_taxable_income_total_by_band__row_36", + "ird_taxable_income_total_by_band__row_37", + "ird_taxable_income_total_by_band__row_38", + "ird_taxable_income_total_by_band__row_39", + "ird_taxable_income_total_by_band__row_40", + "ird_taxable_income_total_by_band__row_41", + "ird_taxable_income_total_by_band__row_42", + "ird_taxable_income_total_by_band__row_43", + "ird_taxable_income_total_by_band__row_44", + "ird_taxable_income_total_by_band__row_45", + "ird_taxable_income_total_by_band__row_46", + "ird_taxable_income_total_by_band__row_47", + "ird_taxable_income_total_by_band__row_48", + "ird_taxable_income_total_by_band__row_49", + "ird_taxable_income_total_by_band__row_50", + "ird_taxable_income_total_by_band__row_51", + "ird_taxable_income_total_by_band__row_52", + "ird_taxable_income_total_by_band__row_53", + "ird_taxable_income_total_by_band__row_54", + "ird_taxable_income_total_by_band__row_55", + "ird_taxable_income_total_by_band__row_56", + "ird_taxable_income_total_by_band__row_57", + "ird_taxable_income_total_by_band__row_58", + "ird_taxable_income_total_by_band__row_59", + "ird_taxable_income_total_by_band__row_60", + "ird_taxable_income_total_by_band__row_61", + "ird_taxable_income_total_by_band__row_62", + "ird_taxable_income_total_by_band__row_63", + "ird_taxable_income_total_by_band__row_64", + "ird_taxable_income_total_by_band__row_65", + "ird_taxable_income_total_by_band__row_66", + "ird_taxable_income_total_by_band__row_67", + "ird_taxable_income_total_by_band__row_68", + "ird_taxable_income_total_by_band__row_69", + "ird_taxable_income_total_by_band__row_70", + "ird_taxable_income_total_by_band__row_71", + "ird_taxable_income_total_by_band__row_72", + "ird_taxable_income_total_by_band__row_73", + "ird_taxable_income_total_by_band__row_74", + "ird_taxable_income_total_by_band__row_75", + "ird_taxable_income_total_by_band__row_76", + "ird_taxable_income_total_by_band__row_77", + "ird_taxable_income_total_by_band__row_78", + "ird_taxable_income_total_by_band__row_79", + "ird_taxable_income_total_by_band__row_80", + "ird_taxable_income_total_by_band__row_81", + "ird_taxable_income_total_by_band__row_82", + "ird_taxable_income_total_by_band__row_83", + "ird_taxable_income_total_by_band__row_84", + "ird_taxable_income_total_by_band__row_85", + "ird_taxable_income_total_by_band__row_86", + "ird_taxable_income_total_by_band__row_87", + "ird_taxable_income_total_by_band__row_88", + "ird_taxable_income_total_by_band__row_89", + "ird_taxable_income_total_by_band__row_90", + "ird_taxable_income_total_by_band__row_91", + "ird_taxable_income_total_by_band__row_92", + "ird_taxable_income_total_by_band__row_93", + "ird_taxable_income_total_by_band__row_94", + "ird_taxable_income_total_by_band__row_95", + "ird_taxable_income_total_by_band__row_96", + "ird_taxable_income_total_by_band__row_97", + "ird_taxable_income_total_by_band__row_98", + "ird_taxable_income_total_by_band__row_99", + "ird_taxable_income_total_by_band__row_100", + "ird_taxable_income_total_by_band__row_101", + "ird_taxable_income_total_by_band__row_102", + "ird_taxable_income_total_by_band__row_103", + "ird_taxable_income_total_by_band__row_104", + "ird_taxable_income_total_by_band__row_105", + "ird_taxable_income_total_by_band__row_106", + "ird_taxable_income_total_by_band__row_107", + "ird_taxable_income_total_by_band__row_108", + "ird_taxable_income_total_by_band__row_109", + "ird_taxable_income_total_by_band__row_110", + "ird_taxable_income_total_by_band__row_111", + "ird_taxable_income_total_by_band__row_112", + "ird_taxable_income_total_by_band__row_113", + "ird_taxable_income_total_by_band__row_114", + "ird_taxable_income_total_by_band__row_115", + "ird_taxable_income_total_by_band__row_116", + "ird_taxable_income_total_by_band__row_117", + "ird_taxable_income_total_by_band__row_118", + "ird_taxable_income_total_by_band__row_119", + "ird_taxable_income_total_by_band__row_120", + "ird_taxable_income_total_by_band__row_121", + "ird_taxable_income_total_by_band__row_122", + "ird_taxable_income_total_by_band__row_123", + "ird_taxable_income_total_by_band__row_124", + "ird_taxable_income_total_by_band__row_125", + "ird_taxable_income_total_by_band__row_126", + "ird_taxable_income_total_by_band__row_127", + "ird_taxable_income_total_by_band__row_128", + "ird_taxable_income_total_by_band__row_129", + "ird_taxable_income_total_by_band__row_130", + "ird_taxable_income_total_by_band__row_131", + "ird_taxable_income_total_by_band__row_132", + "ird_taxable_income_total_by_band__row_133", + "ird_taxable_income_total_by_band__row_134", + "ird_taxable_income_total_by_band__row_135", + "ird_taxable_income_total_by_band__row_136", + "ird_taxable_income_total_by_band__row_137", + "ird_taxable_income_total_by_band__row_138", + "ird_taxable_income_total_by_band__row_139", + "ird_taxable_income_total_by_band__row_140", + "ird_taxable_income_total_by_band__row_141", + "ird_taxable_income_total_by_band__row_142", + "ird_taxable_income_total_by_band__row_143", + "ird_taxable_income_total_by_band__row_144", + "ird_taxable_income_total_by_band__row_145", + "ird_taxable_income_total_by_band__row_146", + "ird_taxable_income_total_by_band__row_147", + "ird_taxable_income_total_by_band__row_148", + "ird_taxable_income_total_by_band__row_149", + "ird_taxable_income_total_by_band__row_150", + "ird_taxable_income_total_by_band__row_151", + "ird_taxable_income_total_by_band__row_152", + "ird_taxable_income_total_by_band__row_153", + "ird_taxable_income_total_by_band__row_154", + "ird_taxable_income_total_by_band__row_155", + "ird_taxable_income_total_by_band__row_156", + "ird_taxable_income_total_by_band__row_157", + "ird_taxable_income_total_by_band__row_158", + "ird_taxable_income_total_by_band__row_159", + "ird_taxable_income_total_by_band__row_160", + "ird_taxable_income_total_by_band__row_161", + "ird_taxable_income_total_by_band__row_162", + "ird_taxable_income_total_by_band__row_163", + "ird_taxable_income_total_by_band__row_164", + "ird_taxable_income_total_by_band__row_165", + "ird_taxable_income_total_by_band__row_166", + "ird_taxable_income_total_by_band__row_167", + "ird_taxable_income_total_by_band__row_168", + "ird_taxable_income_total_by_band__row_169", + "ird_taxable_income_total_by_band__row_170", + "ird_taxable_income_total_by_band__row_171", + "ird_taxable_income_total_by_band__row_172", + "ird_taxable_income_total_by_band__row_173", + "ird_taxable_income_total_by_band__row_174", + "ird_taxable_income_total_by_band__row_175", + "ird_taxable_income_total_by_band__row_176", + "ird_taxable_income_total_by_band__row_177", + "ird_taxable_income_total_by_band__row_178", + "ird_taxable_income_total_by_band__row_179", + "ird_taxable_income_total_by_band__row_180", + "ird_taxable_income_total_by_band__row_181", + "ird_taxable_income_total_by_band__row_182", + "ird_taxable_income_total_by_band__row_183", + "ird_taxable_income_total_by_band__row_184", + "ird_taxable_income_total_by_band__row_185", + "ird_taxable_income_total_by_band__row_186", + "ird_taxable_income_total_by_band__row_187", + "ird_taxable_income_total_by_band__row_188", + "ird_taxable_income_total_by_band__row_189", + "ird_taxable_income_total_by_band__row_190", + "ird_taxable_income_total_by_band__row_191", + "ird_taxable_income_total_by_band__row_192", + "ird_taxable_income_total_by_band__row_193", + "ird_taxable_income_total_by_band__row_194", + "ird_taxable_income_total_by_band__row_195", + "ird_taxable_income_total_by_band__row_196", + "ird_taxable_income_total_by_band__row_197", + "ird_taxable_income_total_by_band__row_198", + "ird_taxable_income_total_by_band__row_199", + "ird_taxable_income_total_by_band__row_200", + "ird_taxable_income_total_by_band__row_201", + "ird_taxable_income_total_by_band__row_202", + "ird_taxable_income_total_by_band__row_203", + "ird_taxable_income_total_by_band__row_204", + "ird_taxable_income_total_by_band__row_205", + "ird_taxable_income_total_by_band__row_206", + "ird_taxable_income_total_by_band__row_207", + "ird_taxable_income_total_by_band__row_208", + "ird_taxable_income_total_by_band__row_209", + "ird_taxable_income_total_by_band__row_210", + "ird_taxable_income_total_by_band__row_211", + "ird_taxable_income_total_by_band__row_212", + "ird_taxable_income_total_by_band__row_213", + "ird_taxable_income_total_by_band__row_214", + "ird_taxable_income_total_by_band__row_215", + "ird_taxable_income_total_by_band__row_216", + "ird_taxable_income_total_by_band__row_217", + "ird_taxable_income_total_by_band__row_218", + "ird_taxable_income_total_by_band__row_219", + "ird_taxable_income_total_by_band__row_220", + "ird_taxable_income_total_by_band__row_221", + "ird_taxable_income_total_by_band__row_222", + "ird_taxable_income_total_by_band__row_223", + "ird_taxable_income_total_by_band__row_224", + "ird_taxable_income_total_by_band__row_225", + "ird_taxable_income_total_by_band__row_226", + "ird_taxable_income_total_by_band__row_227", + "ird_taxable_income_total_by_band__row_228", + "ird_taxable_income_total_by_band__row_229", + "ird_taxable_income_total_by_band__row_230", + "ird_taxable_income_total_by_band__row_231", + "ird_taxable_income_total_by_band__row_232", + "ird_taxable_income_total_by_band__row_233", + "ird_taxable_income_total_by_band__row_234", + "ird_taxable_income_total_by_band__row_235", + "ird_taxable_income_total_by_band__row_236", + "ird_taxable_income_total_by_band__row_237", + "ird_taxable_income_total_by_band__row_238", + "ird_taxable_income_total_by_band__row_239", + "ird_taxable_income_total_by_band__row_240", + "ird_taxable_income_total_by_band__row_241", + "ird_taxable_income_total_by_band__row_242", + "ird_taxable_income_total_by_band__row_243", + "ird_taxable_income_total_by_band__row_244" + ], + "count_reference_names": [ + "ird_taxable_income_people_by_band__row_8", + "ird_taxable_income_people_by_band__row_9", + "ird_taxable_income_people_by_band__row_10", + "ird_taxable_income_people_by_band__row_11", + "ird_taxable_income_people_by_band__row_12", + "ird_taxable_income_people_by_band__row_13", + "ird_taxable_income_people_by_band__row_14", + "ird_taxable_income_people_by_band__row_15", + "ird_taxable_income_people_by_band__row_16", + "ird_taxable_income_people_by_band__row_17", + "ird_taxable_income_people_by_band__row_18", + "ird_taxable_income_people_by_band__row_19", + "ird_taxable_income_people_by_band__row_20", + "ird_taxable_income_people_by_band__row_21", + "ird_taxable_income_people_by_band__row_22", + "ird_taxable_income_people_by_band__row_23", + "ird_taxable_income_people_by_band__row_24", + "ird_taxable_income_people_by_band__row_25", + "ird_taxable_income_people_by_band__row_26", + "ird_taxable_income_people_by_band__row_27", + "ird_taxable_income_people_by_band__row_28", + "ird_taxable_income_people_by_band__row_29", + "ird_taxable_income_people_by_band__row_30", + "ird_taxable_income_people_by_band__row_31", + "ird_taxable_income_people_by_band__row_32", + "ird_taxable_income_people_by_band__row_33", + "ird_taxable_income_people_by_band__row_34", + "ird_taxable_income_people_by_band__row_35", + "ird_taxable_income_people_by_band__row_36", + "ird_taxable_income_people_by_band__row_37", + "ird_taxable_income_people_by_band__row_38", + "ird_taxable_income_people_by_band__row_39", + "ird_taxable_income_people_by_band__row_40", + "ird_taxable_income_people_by_band__row_41", + "ird_taxable_income_people_by_band__row_42", + "ird_taxable_income_people_by_band__row_43", + "ird_taxable_income_people_by_band__row_44", + "ird_taxable_income_people_by_band__row_45", + "ird_taxable_income_people_by_band__row_46", + "ird_taxable_income_people_by_band__row_47", + "ird_taxable_income_people_by_band__row_48", + "ird_taxable_income_people_by_band__row_49", + "ird_taxable_income_people_by_band__row_50", + "ird_taxable_income_people_by_band__row_51", + "ird_taxable_income_people_by_band__row_52", + "ird_taxable_income_people_by_band__row_53", + "ird_taxable_income_people_by_band__row_54", + "ird_taxable_income_people_by_band__row_55", + "ird_taxable_income_people_by_band__row_56", + "ird_taxable_income_people_by_band__row_57", + "ird_taxable_income_people_by_band__row_58", + "ird_taxable_income_people_by_band__row_59", + "ird_taxable_income_people_by_band__row_60", + "ird_taxable_income_people_by_band__row_61", + "ird_taxable_income_people_by_band__row_62", + "ird_taxable_income_people_by_band__row_63", + "ird_taxable_income_people_by_band__row_64", + "ird_taxable_income_people_by_band__row_65", + "ird_taxable_income_people_by_band__row_66", + "ird_taxable_income_people_by_band__row_67", + "ird_taxable_income_people_by_band__row_68", + "ird_taxable_income_people_by_band__row_69", + "ird_taxable_income_people_by_band__row_70", + "ird_taxable_income_people_by_band__row_71", + "ird_taxable_income_people_by_band__row_72", + "ird_taxable_income_people_by_band__row_73", + "ird_taxable_income_people_by_band__row_74", + "ird_taxable_income_people_by_band__row_75", + "ird_taxable_income_people_by_band__row_76", + "ird_taxable_income_people_by_band__row_77", + "ird_taxable_income_people_by_band__row_78", + "ird_taxable_income_people_by_band__row_79", + "ird_taxable_income_people_by_band__row_80", + "ird_taxable_income_people_by_band__row_81", + "ird_taxable_income_people_by_band__row_82", + "ird_taxable_income_people_by_band__row_83", + "ird_taxable_income_people_by_band__row_84", + "ird_taxable_income_people_by_band__row_85", + "ird_taxable_income_people_by_band__row_86", + "ird_taxable_income_people_by_band__row_87", + "ird_taxable_income_people_by_band__row_88", + "ird_taxable_income_people_by_band__row_89", + "ird_taxable_income_people_by_band__row_90", + "ird_taxable_income_people_by_band__row_91", + "ird_taxable_income_people_by_band__row_92", + "ird_taxable_income_people_by_band__row_93", + "ird_taxable_income_people_by_band__row_94", + "ird_taxable_income_people_by_band__row_95", + "ird_taxable_income_people_by_band__row_96", + "ird_taxable_income_people_by_band__row_97", + "ird_taxable_income_people_by_band__row_98", + "ird_taxable_income_people_by_band__row_99", + "ird_taxable_income_people_by_band__row_100", + "ird_taxable_income_people_by_band__row_101", + "ird_taxable_income_people_by_band__row_102", + "ird_taxable_income_people_by_band__row_103", + "ird_taxable_income_people_by_band__row_104", + "ird_taxable_income_people_by_band__row_105", + "ird_taxable_income_people_by_band__row_106", + "ird_taxable_income_people_by_band__row_107", + "ird_taxable_income_people_by_band__row_108", + "ird_taxable_income_people_by_band__row_109", + "ird_taxable_income_people_by_band__row_110", + "ird_taxable_income_people_by_band__row_111", + "ird_taxable_income_people_by_band__row_112", + "ird_taxable_income_people_by_band__row_113", + "ird_taxable_income_people_by_band__row_114", + "ird_taxable_income_people_by_band__row_115", + "ird_taxable_income_people_by_band__row_116", + "ird_taxable_income_people_by_band__row_117", + "ird_taxable_income_people_by_band__row_118", + "ird_taxable_income_people_by_band__row_119", + "ird_taxable_income_people_by_band__row_120", + "ird_taxable_income_people_by_band__row_121", + "ird_taxable_income_people_by_band__row_122", + "ird_taxable_income_people_by_band__row_123", + "ird_taxable_income_people_by_band__row_124", + "ird_taxable_income_people_by_band__row_125", + "ird_taxable_income_people_by_band__row_126", + "ird_taxable_income_people_by_band__row_127", + "ird_taxable_income_people_by_band__row_128", + "ird_taxable_income_people_by_band__row_129", + "ird_taxable_income_people_by_band__row_130", + "ird_taxable_income_people_by_band__row_131", + "ird_taxable_income_people_by_band__row_132", + "ird_taxable_income_people_by_band__row_133", + "ird_taxable_income_people_by_band__row_134", + "ird_taxable_income_people_by_band__row_135", + "ird_taxable_income_people_by_band__row_136", + "ird_taxable_income_people_by_band__row_137", + "ird_taxable_income_people_by_band__row_138", + "ird_taxable_income_people_by_band__row_139", + "ird_taxable_income_people_by_band__row_140", + "ird_taxable_income_people_by_band__row_141", + "ird_taxable_income_people_by_band__row_142", + "ird_taxable_income_people_by_band__row_143", + "ird_taxable_income_people_by_band__row_144", + "ird_taxable_income_people_by_band__row_145", + "ird_taxable_income_people_by_band__row_146", + "ird_taxable_income_people_by_band__row_147", + "ird_taxable_income_people_by_band__row_148", + "ird_taxable_income_people_by_band__row_149", + "ird_taxable_income_people_by_band__row_150", + "ird_taxable_income_people_by_band__row_151", + "ird_taxable_income_people_by_band__row_152", + "ird_taxable_income_people_by_band__row_153", + "ird_taxable_income_people_by_band__row_154", + "ird_taxable_income_people_by_band__row_155", + "ird_taxable_income_people_by_band__row_156", + "ird_taxable_income_people_by_band__row_157", + "ird_taxable_income_people_by_band__row_158", + "ird_taxable_income_people_by_band__row_159", + "ird_taxable_income_people_by_band__row_160", + "ird_taxable_income_people_by_band__row_161", + "ird_taxable_income_people_by_band__row_162", + "ird_taxable_income_people_by_band__row_163", + "ird_taxable_income_people_by_band__row_164", + "ird_taxable_income_people_by_band__row_165", + "ird_taxable_income_people_by_band__row_166", + "ird_taxable_income_people_by_band__row_167", + "ird_taxable_income_people_by_band__row_168", + "ird_taxable_income_people_by_band__row_169", + "ird_taxable_income_people_by_band__row_170", + "ird_taxable_income_people_by_band__row_171", + "ird_taxable_income_people_by_band__row_172", + "ird_taxable_income_people_by_band__row_173", + "ird_taxable_income_people_by_band__row_174", + "ird_taxable_income_people_by_band__row_175", + "ird_taxable_income_people_by_band__row_176", + "ird_taxable_income_people_by_band__row_177", + "ird_taxable_income_people_by_band__row_178", + "ird_taxable_income_people_by_band__row_179", + "ird_taxable_income_people_by_band__row_180", + "ird_taxable_income_people_by_band__row_181", + "ird_taxable_income_people_by_band__row_182", + "ird_taxable_income_people_by_band__row_183", + "ird_taxable_income_people_by_band__row_184", + "ird_taxable_income_people_by_band__row_185", + "ird_taxable_income_people_by_band__row_186", + "ird_taxable_income_people_by_band__row_187", + "ird_taxable_income_people_by_band__row_188", + "ird_taxable_income_people_by_band__row_189", + "ird_taxable_income_people_by_band__row_190", + "ird_taxable_income_people_by_band__row_191", + "ird_taxable_income_people_by_band__row_192", + "ird_taxable_income_people_by_band__row_193", + "ird_taxable_income_people_by_band__row_194", + "ird_taxable_income_people_by_band__row_195", + "ird_taxable_income_people_by_band__row_196", + "ird_taxable_income_people_by_band__row_197", + "ird_taxable_income_people_by_band__row_198", + "ird_taxable_income_people_by_band__row_199", + "ird_taxable_income_people_by_band__row_200", + "ird_taxable_income_people_by_band__row_201", + "ird_taxable_income_people_by_band__row_202", + "ird_taxable_income_people_by_band__row_203", + "ird_taxable_income_people_by_band__row_204", + "ird_taxable_income_people_by_band__row_205", + "ird_taxable_income_people_by_band__row_206", + "ird_taxable_income_people_by_band__row_207", + "ird_taxable_income_people_by_band__row_208", + "ird_taxable_income_people_by_band__row_209", + "ird_taxable_income_people_by_band__row_210", + "ird_taxable_income_people_by_band__row_211", + "ird_taxable_income_people_by_band__row_212", + "ird_taxable_income_people_by_band__row_213", + "ird_taxable_income_people_by_band__row_214", + "ird_taxable_income_people_by_band__row_215", + "ird_taxable_income_people_by_band__row_216", + "ird_taxable_income_people_by_band__row_217", + "ird_taxable_income_people_by_band__row_218", + "ird_taxable_income_people_by_band__row_219", + "ird_taxable_income_people_by_band__row_220", + "ird_taxable_income_people_by_band__row_221", + "ird_taxable_income_people_by_band__row_222", + "ird_taxable_income_people_by_band__row_223", + "ird_taxable_income_people_by_band__row_224", + "ird_taxable_income_people_by_band__row_225", + "ird_taxable_income_people_by_band__row_226", + "ird_taxable_income_people_by_band__row_227", + "ird_taxable_income_people_by_band__row_228", + "ird_taxable_income_people_by_band__row_229", + "ird_taxable_income_people_by_band__row_230", + "ird_taxable_income_people_by_band__row_231", + "ird_taxable_income_people_by_band__row_232", + "ird_taxable_income_people_by_band__row_233", + "ird_taxable_income_people_by_band__row_234", + "ird_taxable_income_people_by_band__row_235", + "ird_taxable_income_people_by_band__row_236", + "ird_taxable_income_people_by_band__row_237", + "ird_taxable_income_people_by_band__row_238", + "ird_taxable_income_people_by_band__row_239", + "ird_taxable_income_people_by_band__row_240", + "ird_taxable_income_people_by_band__row_241", + "ird_taxable_income_people_by_band__row_242", + "ird_taxable_income_people_by_band__row_243", + "ird_taxable_income_people_by_band__row_244" + ], + "interval_table_ids": [], + "required_measurement_binding": "ird_taxable_income_ty2027_v1" + } + ] } } } diff --git a/packages/microcosm-build/src/microcosm/build/nz/target_references.json b/packages/microcosm-build/src/microcosm/build/nz/target_references.json index bbe44aee98..8137a17bc3 100644 --- a/packages/microcosm-build/src/microcosm/build/nz/target_references.json +++ b/packages/microcosm-build/src/microcosm/build/nz/target_references.json @@ -5,7 +5,8 @@ "allowed_value_operations": [ "identity", "rolled_forward_by_ratio", - "allocated_residual_by_ratio" + "allocated_residual_by_ratio", + "linear_combination" ], "target_references": [ { @@ -25597,14 +25598,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -25619,7 +25624,7 @@ "target_label": "TY2026-27 ird taxable income people by band nil (parked)" }, "filter": "taxable_income_band_row_8_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_9", @@ -25649,14 +25654,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -25671,7 +25680,7 @@ "target_label": "TY2026-27 ird taxable income people by band $0.01 - $100 (parked)" }, "filter": "taxable_income_band_row_9_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_10", @@ -25701,14 +25710,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -25723,7 +25736,7 @@ "target_label": "TY2026-27 ird taxable income people by band $101 - $1,000 (parked)" }, "filter": "taxable_income_band_row_10_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_11", @@ -25753,14 +25766,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -25775,7 +25792,7 @@ "target_label": "TY2026-27 ird taxable income people by band $1,001 - $2,000 (parked)" }, "filter": "taxable_income_band_row_11_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_12", @@ -25805,14 +25822,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -25827,7 +25848,7 @@ "target_label": "TY2026-27 ird taxable income people by band $2,001 - $3,000 (parked)" }, "filter": "taxable_income_band_row_12_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_13", @@ -25857,14 +25878,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -25879,7 +25904,7 @@ "target_label": "TY2026-27 ird taxable income people by band $3,001 - $4,000 (parked)" }, "filter": "taxable_income_band_row_13_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_14", @@ -25909,14 +25934,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -25931,7 +25960,7 @@ "target_label": "TY2026-27 ird taxable income people by band $4,001 - $5,000 (parked)" }, "filter": "taxable_income_band_row_14_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_15", @@ -25961,14 +25990,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -25983,7 +26016,7 @@ "target_label": "TY2026-27 ird taxable income people by band $5,001 - $6,000 (parked)" }, "filter": "taxable_income_band_row_15_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_16", @@ -26013,14 +26046,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -26035,7 +26072,7 @@ "target_label": "TY2026-27 ird taxable income people by band $6,001 - $7,000 (parked)" }, "filter": "taxable_income_band_row_16_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_17", @@ -26065,14 +26102,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -26087,7 +26128,7 @@ "target_label": "TY2026-27 ird taxable income people by band $7,001 - $8,000 (parked)" }, "filter": "taxable_income_band_row_17_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_18", @@ -26117,14 +26158,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -26139,7 +26184,7 @@ "target_label": "TY2026-27 ird taxable income people by band $8,001 - $9,000 (parked)" }, "filter": "taxable_income_band_row_18_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_19", @@ -26169,14 +26214,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -26191,7 +26240,7 @@ "target_label": "TY2026-27 ird taxable income people by band $9,001 - $10,000 (parked)" }, "filter": "taxable_income_band_row_19_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_20", @@ -26221,14 +26270,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -26243,7 +26296,7 @@ "target_label": "TY2026-27 ird taxable income people by band $10,001 - $11,000 (parked)" }, "filter": "taxable_income_band_row_20_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_21", @@ -26273,14 +26326,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -26295,7 +26352,7 @@ "target_label": "TY2026-27 ird taxable income people by band $11,001 - $12,000 (parked)" }, "filter": "taxable_income_band_row_21_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_22", @@ -26325,14 +26382,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -26347,7 +26408,7 @@ "target_label": "TY2026-27 ird taxable income people by band $12,001 - $13,000 (parked)" }, "filter": "taxable_income_band_row_22_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_23", @@ -26377,14 +26438,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -26399,7 +26464,7 @@ "target_label": "TY2026-27 ird taxable income people by band $13,001 - $14,000 (parked)" }, "filter": "taxable_income_band_row_23_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_24", @@ -26429,14 +26494,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -26451,7 +26520,7 @@ "target_label": "TY2026-27 ird taxable income people by band $14,001 - $15,000 (parked)" }, "filter": "taxable_income_band_row_24_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_25", @@ -26481,14 +26550,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -26503,7 +26576,7 @@ "target_label": "TY2026-27 ird taxable income people by band $15,001 - $16,000 (parked)" }, "filter": "taxable_income_band_row_25_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_26", @@ -26533,14 +26606,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -26555,7 +26632,7 @@ "target_label": "TY2026-27 ird taxable income people by band $16,001 - $17,000 (parked)" }, "filter": "taxable_income_band_row_26_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_27", @@ -26585,14 +26662,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -26607,7 +26688,7 @@ "target_label": "TY2026-27 ird taxable income people by band $17,001 - $18,000 (parked)" }, "filter": "taxable_income_band_row_27_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_28", @@ -26637,14 +26718,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -26659,7 +26744,7 @@ "target_label": "TY2026-27 ird taxable income people by band $18,001 - $19,000 (parked)" }, "filter": "taxable_income_band_row_28_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_29", @@ -26689,14 +26774,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -26711,7 +26800,7 @@ "target_label": "TY2026-27 ird taxable income people by band $19,001 - $20,000 (parked)" }, "filter": "taxable_income_band_row_29_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_30", @@ -26741,14 +26830,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -26763,7 +26856,7 @@ "target_label": "TY2026-27 ird taxable income people by band $20,000 - $21,000 (parked)" }, "filter": "taxable_income_band_row_30_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_31", @@ -26793,14 +26886,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -26815,7 +26912,7 @@ "target_label": "TY2026-27 ird taxable income people by band $21,001 - $22,000 (parked)" }, "filter": "taxable_income_band_row_31_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_32", @@ -26845,14 +26942,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -26867,7 +26968,7 @@ "target_label": "TY2026-27 ird taxable income people by band $22,001 - $23,000 (parked)" }, "filter": "taxable_income_band_row_32_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_33", @@ -26897,14 +26998,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -26919,7 +27024,7 @@ "target_label": "TY2026-27 ird taxable income people by band $23,001 - $24,000 (parked)" }, "filter": "taxable_income_band_row_33_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_34", @@ -26949,14 +27054,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -26971,7 +27080,7 @@ "target_label": "TY2026-27 ird taxable income people by band $24,001 - $25,000 (parked)" }, "filter": "taxable_income_band_row_34_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_35", @@ -27001,14 +27110,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -27023,7 +27136,7 @@ "target_label": "TY2026-27 ird taxable income people by band $25,001 - $26,000 (parked)" }, "filter": "taxable_income_band_row_35_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_36", @@ -27053,14 +27166,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -27075,7 +27192,7 @@ "target_label": "TY2026-27 ird taxable income people by band $26,001 - $27,000 (parked)" }, "filter": "taxable_income_band_row_36_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_37", @@ -27105,14 +27222,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -27127,7 +27248,7 @@ "target_label": "TY2026-27 ird taxable income people by band $27,001 - $28,000 (parked)" }, "filter": "taxable_income_band_row_37_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_38", @@ -27157,14 +27278,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -27179,7 +27304,7 @@ "target_label": "TY2026-27 ird taxable income people by band $28,001 - $29,000 (parked)" }, "filter": "taxable_income_band_row_38_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_39", @@ -27209,14 +27334,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -27231,7 +27360,7 @@ "target_label": "TY2026-27 ird taxable income people by band $29,001 - $30,000 (parked)" }, "filter": "taxable_income_band_row_39_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_40", @@ -27261,14 +27390,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -27283,7 +27416,7 @@ "target_label": "TY2026-27 ird taxable income people by band $30,001 - $31,000 (parked)" }, "filter": "taxable_income_band_row_40_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_41", @@ -27313,14 +27446,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -27335,7 +27472,7 @@ "target_label": "TY2026-27 ird taxable income people by band $31,001 - $32,000 (parked)" }, "filter": "taxable_income_band_row_41_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_42", @@ -27365,14 +27502,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -27387,7 +27528,7 @@ "target_label": "TY2026-27 ird taxable income people by band $32,001 - $33,000 (parked)" }, "filter": "taxable_income_band_row_42_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_43", @@ -27417,14 +27558,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -27439,7 +27584,7 @@ "target_label": "TY2026-27 ird taxable income people by band $33,001 - $34,000 (parked)" }, "filter": "taxable_income_band_row_43_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_44", @@ -27469,14 +27614,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -27491,7 +27640,7 @@ "target_label": "TY2026-27 ird taxable income people by band $34,001 - $35,000 (parked)" }, "filter": "taxable_income_band_row_44_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_45", @@ -27521,14 +27670,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -27543,7 +27696,7 @@ "target_label": "TY2026-27 ird taxable income people by band $35,001 - $36,000 (parked)" }, "filter": "taxable_income_band_row_45_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_46", @@ -27573,14 +27726,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -27595,7 +27752,7 @@ "target_label": "TY2026-27 ird taxable income people by band $36,001 - $37,000 (parked)" }, "filter": "taxable_income_band_row_46_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_47", @@ -27625,14 +27782,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -27647,7 +27808,7 @@ "target_label": "TY2026-27 ird taxable income people by band $37,001 - $38,000 (parked)" }, "filter": "taxable_income_band_row_47_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_48", @@ -27677,14 +27838,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -27699,7 +27864,7 @@ "target_label": "TY2026-27 ird taxable income people by band $38,001 - $39,000 (parked)" }, "filter": "taxable_income_band_row_48_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_49", @@ -27729,14 +27894,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -27751,7 +27920,7 @@ "target_label": "TY2026-27 ird taxable income people by band $39,001 - $40,000 (parked)" }, "filter": "taxable_income_band_row_49_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_50", @@ -27781,14 +27950,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -27803,7 +27976,7 @@ "target_label": "TY2026-27 ird taxable income people by band $40,001 - $41,000 (parked)" }, "filter": "taxable_income_band_row_50_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_51", @@ -27833,14 +28006,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -27855,7 +28032,7 @@ "target_label": "TY2026-27 ird taxable income people by band $41,001 - $42,000 (parked)" }, "filter": "taxable_income_band_row_51_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_52", @@ -27885,14 +28062,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -27907,7 +28088,7 @@ "target_label": "TY2026-27 ird taxable income people by band $42,001 - $43,000 (parked)" }, "filter": "taxable_income_band_row_52_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_53", @@ -27937,14 +28118,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -27959,7 +28144,7 @@ "target_label": "TY2026-27 ird taxable income people by band $43,001 - $44,000 (parked)" }, "filter": "taxable_income_band_row_53_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_54", @@ -27989,14 +28174,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -28011,7 +28200,7 @@ "target_label": "TY2026-27 ird taxable income people by band $44,001 - $45,000 (parked)" }, "filter": "taxable_income_band_row_54_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_55", @@ -28041,14 +28230,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -28063,7 +28256,7 @@ "target_label": "TY2026-27 ird taxable income people by band $45,001 - $46,000 (parked)" }, "filter": "taxable_income_band_row_55_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_56", @@ -28093,14 +28286,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -28115,7 +28312,7 @@ "target_label": "TY2026-27 ird taxable income people by band $46,001 - $47,000 (parked)" }, "filter": "taxable_income_band_row_56_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_57", @@ -28145,14 +28342,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -28167,7 +28368,7 @@ "target_label": "TY2026-27 ird taxable income people by band $47,001 - $48,000 (parked)" }, "filter": "taxable_income_band_row_57_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_58", @@ -28197,14 +28398,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -28219,7 +28424,7 @@ "target_label": "TY2026-27 ird taxable income people by band $48,001 - $49,000 (parked)" }, "filter": "taxable_income_band_row_58_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_59", @@ -28249,14 +28454,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -28271,7 +28480,7 @@ "target_label": "TY2026-27 ird taxable income people by band $49,001 - $50,000 (parked)" }, "filter": "taxable_income_band_row_59_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_60", @@ -28301,14 +28510,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -28323,7 +28536,7 @@ "target_label": "TY2026-27 ird taxable income people by band $50,000 - $51,000 (parked)" }, "filter": "taxable_income_band_row_60_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_61", @@ -28353,14 +28566,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -28375,7 +28592,7 @@ "target_label": "TY2026-27 ird taxable income people by band $51,001 - $52,000 (parked)" }, "filter": "taxable_income_band_row_61_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_62", @@ -28405,14 +28622,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -28427,7 +28648,7 @@ "target_label": "TY2026-27 ird taxable income people by band $52,001 - $53,000 (parked)" }, "filter": "taxable_income_band_row_62_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_63", @@ -28457,14 +28678,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -28479,7 +28704,7 @@ "target_label": "TY2026-27 ird taxable income people by band $53,001 - $54,000 (parked)" }, "filter": "taxable_income_band_row_63_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_64", @@ -28509,14 +28734,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -28531,7 +28760,7 @@ "target_label": "TY2026-27 ird taxable income people by band $54,001 - $55,000 (parked)" }, "filter": "taxable_income_band_row_64_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_65", @@ -28561,14 +28790,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -28583,7 +28816,7 @@ "target_label": "TY2026-27 ird taxable income people by band $55,001 - $56,000 (parked)" }, "filter": "taxable_income_band_row_65_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_66", @@ -28613,14 +28846,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -28635,7 +28872,7 @@ "target_label": "TY2026-27 ird taxable income people by band $56,001 - $57,000 (parked)" }, "filter": "taxable_income_band_row_66_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_67", @@ -28665,14 +28902,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -28687,7 +28928,7 @@ "target_label": "TY2026-27 ird taxable income people by band $57,001 - $58,000 (parked)" }, "filter": "taxable_income_band_row_67_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_68", @@ -28717,14 +28958,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -28739,7 +28984,7 @@ "target_label": "TY2026-27 ird taxable income people by band $58,001 - $59,000 (parked)" }, "filter": "taxable_income_band_row_68_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_69", @@ -28769,14 +29014,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -28791,7 +29040,7 @@ "target_label": "TY2026-27 ird taxable income people by band $59,001 - $60,000 (parked)" }, "filter": "taxable_income_band_row_69_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_70", @@ -28821,14 +29070,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -28843,7 +29096,7 @@ "target_label": "TY2026-27 ird taxable income people by band $60,001 - $61,000 (parked)" }, "filter": "taxable_income_band_row_70_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_71", @@ -28873,14 +29126,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -28895,7 +29152,7 @@ "target_label": "TY2026-27 ird taxable income people by band $61,001 - $62,000 (parked)" }, "filter": "taxable_income_band_row_71_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_72", @@ -28925,14 +29182,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -28947,7 +29208,7 @@ "target_label": "TY2026-27 ird taxable income people by band $62,001 - $63,000 (parked)" }, "filter": "taxable_income_band_row_72_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_73", @@ -28977,14 +29238,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -28999,7 +29264,7 @@ "target_label": "TY2026-27 ird taxable income people by band $63,001 - $64,000 (parked)" }, "filter": "taxable_income_band_row_73_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_74", @@ -29029,14 +29294,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -29051,7 +29320,7 @@ "target_label": "TY2026-27 ird taxable income people by band $64,001 - $65,000 (parked)" }, "filter": "taxable_income_band_row_74_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_75", @@ -29081,14 +29350,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -29103,7 +29376,7 @@ "target_label": "TY2026-27 ird taxable income people by band $65,001 - $66,000 (parked)" }, "filter": "taxable_income_band_row_75_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_76", @@ -29133,14 +29406,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -29155,7 +29432,7 @@ "target_label": "TY2026-27 ird taxable income people by band $66,001 - $67,000 (parked)" }, "filter": "taxable_income_band_row_76_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_77", @@ -29185,14 +29462,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -29207,7 +29488,7 @@ "target_label": "TY2026-27 ird taxable income people by band $67,001 - $68,000 (parked)" }, "filter": "taxable_income_band_row_77_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_78", @@ -29237,14 +29518,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -29259,7 +29544,7 @@ "target_label": "TY2026-27 ird taxable income people by band $68,001 - $69,000 (parked)" }, "filter": "taxable_income_band_row_78_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_79", @@ -29289,14 +29574,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -29311,7 +29600,7 @@ "target_label": "TY2026-27 ird taxable income people by band $69,001 - $70,000 (parked)" }, "filter": "taxable_income_band_row_79_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_80", @@ -29341,14 +29630,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -29363,7 +29656,7 @@ "target_label": "TY2026-27 ird taxable income people by band $70,001 - $71,000 (parked)" }, "filter": "taxable_income_band_row_80_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_81", @@ -29393,14 +29686,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -29415,7 +29712,7 @@ "target_label": "TY2026-27 ird taxable income people by band $71,001 - $72,000 (parked)" }, "filter": "taxable_income_band_row_81_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_82", @@ -29445,14 +29742,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -29467,7 +29768,7 @@ "target_label": "TY2026-27 ird taxable income people by band $72,001 - $73,000 (parked)" }, "filter": "taxable_income_band_row_82_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_83", @@ -29497,14 +29798,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -29519,7 +29824,7 @@ "target_label": "TY2026-27 ird taxable income people by band $73,001 - $74,000 (parked)" }, "filter": "taxable_income_band_row_83_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_84", @@ -29549,14 +29854,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -29571,7 +29880,7 @@ "target_label": "TY2026-27 ird taxable income people by band $74,001 - $75,000 (parked)" }, "filter": "taxable_income_band_row_84_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_85", @@ -29601,14 +29910,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -29623,7 +29936,7 @@ "target_label": "TY2026-27 ird taxable income people by band $75,001 - $76,000 (parked)" }, "filter": "taxable_income_band_row_85_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_86", @@ -29653,14 +29966,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -29675,7 +29992,7 @@ "target_label": "TY2026-27 ird taxable income people by band $76,001 - $77,000 (parked)" }, "filter": "taxable_income_band_row_86_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_87", @@ -29705,14 +30022,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -29727,7 +30048,7 @@ "target_label": "TY2026-27 ird taxable income people by band $77,001 - $78,000 (parked)" }, "filter": "taxable_income_band_row_87_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_88", @@ -29757,14 +30078,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -29779,7 +30104,7 @@ "target_label": "TY2026-27 ird taxable income people by band $78,001 - $79,000 (parked)" }, "filter": "taxable_income_band_row_88_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_89", @@ -29809,14 +30134,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -29831,7 +30160,7 @@ "target_label": "TY2026-27 ird taxable income people by band $79,001 - $80,000 (parked)" }, "filter": "taxable_income_band_row_89_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_90", @@ -29861,14 +30190,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -29883,7 +30216,7 @@ "target_label": "TY2026-27 ird taxable income people by band $80,000 - $81,000 (parked)" }, "filter": "taxable_income_band_row_90_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_91", @@ -29913,14 +30246,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -29935,7 +30272,7 @@ "target_label": "TY2026-27 ird taxable income people by band $81,001 - $82,000 (parked)" }, "filter": "taxable_income_band_row_91_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_92", @@ -29965,14 +30302,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -29987,7 +30328,7 @@ "target_label": "TY2026-27 ird taxable income people by band $82,001 - $83,000 (parked)" }, "filter": "taxable_income_band_row_92_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_93", @@ -30017,14 +30358,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -30039,7 +30384,7 @@ "target_label": "TY2026-27 ird taxable income people by band $83,001 - $84,000 (parked)" }, "filter": "taxable_income_band_row_93_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_94", @@ -30069,14 +30414,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -30091,7 +30440,7 @@ "target_label": "TY2026-27 ird taxable income people by band $84,001 - $85,000 (parked)" }, "filter": "taxable_income_band_row_94_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_95", @@ -30121,14 +30470,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -30143,7 +30496,7 @@ "target_label": "TY2026-27 ird taxable income people by band $85,001 - $86,000 (parked)" }, "filter": "taxable_income_band_row_95_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_96", @@ -30173,14 +30526,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -30195,7 +30552,7 @@ "target_label": "TY2026-27 ird taxable income people by band $86,001 - $87,000 (parked)" }, "filter": "taxable_income_band_row_96_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_97", @@ -30225,14 +30582,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -30247,7 +30608,7 @@ "target_label": "TY2026-27 ird taxable income people by band $87,001 - $88,000 (parked)" }, "filter": "taxable_income_band_row_97_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_98", @@ -30277,14 +30638,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -30299,7 +30664,7 @@ "target_label": "TY2026-27 ird taxable income people by band $88,001 - $89,000 (parked)" }, "filter": "taxable_income_band_row_98_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_99", @@ -30329,14 +30694,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -30351,7 +30720,7 @@ "target_label": "TY2026-27 ird taxable income people by band $89,001 - $90,000 (parked)" }, "filter": "taxable_income_band_row_99_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_100", @@ -30381,14 +30750,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -30403,7 +30776,7 @@ "target_label": "TY2026-27 ird taxable income people by band $90,001 - $91,000 (parked)" }, "filter": "taxable_income_band_row_100_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_101", @@ -30433,14 +30806,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -30455,7 +30832,7 @@ "target_label": "TY2026-27 ird taxable income people by band $91,001 - $92,000 (parked)" }, "filter": "taxable_income_band_row_101_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_102", @@ -30485,14 +30862,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -30507,7 +30888,7 @@ "target_label": "TY2026-27 ird taxable income people by band $92,001 - $93,000 (parked)" }, "filter": "taxable_income_band_row_102_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_103", @@ -30537,14 +30918,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -30559,7 +30944,7 @@ "target_label": "TY2026-27 ird taxable income people by band $93,001 - $94,000 (parked)" }, "filter": "taxable_income_band_row_103_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_104", @@ -30589,14 +30974,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -30611,7 +31000,7 @@ "target_label": "TY2026-27 ird taxable income people by band $94,001 - $95,000 (parked)" }, "filter": "taxable_income_band_row_104_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_105", @@ -30641,14 +31030,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -30663,7 +31056,7 @@ "target_label": "TY2026-27 ird taxable income people by band $95,001 - $96,000 (parked)" }, "filter": "taxable_income_band_row_105_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_106", @@ -30693,14 +31086,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -30715,7 +31112,7 @@ "target_label": "TY2026-27 ird taxable income people by band $96,001 - $97,000 (parked)" }, "filter": "taxable_income_band_row_106_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_107", @@ -30745,14 +31142,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -30767,7 +31168,7 @@ "target_label": "TY2026-27 ird taxable income people by band $97,001 - $98,000 (parked)" }, "filter": "taxable_income_band_row_107_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_108", @@ -30797,14 +31198,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -30819,7 +31224,7 @@ "target_label": "TY2026-27 ird taxable income people by band $98,001 - $99,000 (parked)" }, "filter": "taxable_income_band_row_108_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_109", @@ -30849,14 +31254,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -30871,7 +31280,7 @@ "target_label": "TY2026-27 ird taxable income people by band $99,001 - $100,000 (parked)" }, "filter": "taxable_income_band_row_109_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_110", @@ -30901,14 +31310,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -30923,7 +31336,7 @@ "target_label": "TY2026-27 ird taxable income people by band $100,001 - $101,000 (parked)" }, "filter": "taxable_income_band_row_110_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_111", @@ -30953,14 +31366,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -30975,7 +31392,7 @@ "target_label": "TY2026-27 ird taxable income people by band $101,001 - $102,000 (parked)" }, "filter": "taxable_income_band_row_111_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_112", @@ -31005,14 +31422,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -31027,7 +31448,7 @@ "target_label": "TY2026-27 ird taxable income people by band $102,001 - $103,000 (parked)" }, "filter": "taxable_income_band_row_112_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_113", @@ -31057,14 +31478,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -31079,7 +31504,7 @@ "target_label": "TY2026-27 ird taxable income people by band $103,001 - $104,000 (parked)" }, "filter": "taxable_income_band_row_113_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_114", @@ -31109,14 +31534,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -31131,7 +31560,7 @@ "target_label": "TY2026-27 ird taxable income people by band $104,001 - $105,000 (parked)" }, "filter": "taxable_income_band_row_114_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_115", @@ -31161,14 +31590,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -31183,7 +31616,7 @@ "target_label": "TY2026-27 ird taxable income people by band $105,001 - $106,000 (parked)" }, "filter": "taxable_income_band_row_115_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_116", @@ -31213,14 +31646,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -31235,7 +31672,7 @@ "target_label": "TY2026-27 ird taxable income people by band $106,001 - $107,000 (parked)" }, "filter": "taxable_income_band_row_116_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_117", @@ -31265,14 +31702,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -31287,7 +31728,7 @@ "target_label": "TY2026-27 ird taxable income people by band $107,001 - $108,000 (parked)" }, "filter": "taxable_income_band_row_117_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_118", @@ -31317,14 +31758,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -31339,7 +31784,7 @@ "target_label": "TY2026-27 ird taxable income people by band $108,001 - $109,000 (parked)" }, "filter": "taxable_income_band_row_118_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_119", @@ -31369,14 +31814,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -31391,7 +31840,7 @@ "target_label": "TY2026-27 ird taxable income people by band $109,001 - $110,000 (parked)" }, "filter": "taxable_income_band_row_119_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_120", @@ -31421,14 +31870,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -31443,7 +31896,7 @@ "target_label": "TY2026-27 ird taxable income people by band $110,001 - $111,000 (parked)" }, "filter": "taxable_income_band_row_120_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_121", @@ -31473,14 +31926,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -31495,7 +31952,7 @@ "target_label": "TY2026-27 ird taxable income people by band $111,001 - $112,000 (parked)" }, "filter": "taxable_income_band_row_121_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_122", @@ -31525,14 +31982,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -31547,7 +32008,7 @@ "target_label": "TY2026-27 ird taxable income people by band $112,001 - $113,000 (parked)" }, "filter": "taxable_income_band_row_122_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_123", @@ -31577,14 +32038,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -31599,7 +32064,7 @@ "target_label": "TY2026-27 ird taxable income people by band $113,001 - $114,000 (parked)" }, "filter": "taxable_income_band_row_123_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_124", @@ -31629,14 +32094,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -31651,7 +32120,7 @@ "target_label": "TY2026-27 ird taxable income people by band $114,001 - $115,000 (parked)" }, "filter": "taxable_income_band_row_124_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_125", @@ -31681,14 +32150,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -31703,7 +32176,7 @@ "target_label": "TY2026-27 ird taxable income people by band $115,001 - $116,000 (parked)" }, "filter": "taxable_income_band_row_125_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_126", @@ -31733,14 +32206,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -31755,7 +32232,7 @@ "target_label": "TY2026-27 ird taxable income people by band $116,001 - $117,000 (parked)" }, "filter": "taxable_income_band_row_126_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_127", @@ -31785,14 +32262,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -31807,7 +32288,7 @@ "target_label": "TY2026-27 ird taxable income people by band $117,001 - $118,000 (parked)" }, "filter": "taxable_income_band_row_127_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_128", @@ -31837,14 +32318,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -31859,7 +32344,7 @@ "target_label": "TY2026-27 ird taxable income people by band $118,001 - $119,000 (parked)" }, "filter": "taxable_income_band_row_128_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_129", @@ -31889,14 +32374,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -31911,7 +32400,7 @@ "target_label": "TY2026-27 ird taxable income people by band $119,001 - $120,000 (parked)" }, "filter": "taxable_income_band_row_129_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_130", @@ -31941,14 +32430,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -31963,7 +32456,7 @@ "target_label": "TY2026-27 ird taxable income people by band $120,001 - $121,000 (parked)" }, "filter": "taxable_income_band_row_130_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_131", @@ -31993,14 +32486,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -32015,7 +32512,7 @@ "target_label": "TY2026-27 ird taxable income people by band $121,001 - $122,000 (parked)" }, "filter": "taxable_income_band_row_131_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_132", @@ -32045,14 +32542,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -32067,7 +32568,7 @@ "target_label": "TY2026-27 ird taxable income people by band $122,001 - $123,000 (parked)" }, "filter": "taxable_income_band_row_132_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_133", @@ -32097,14 +32598,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -32119,7 +32624,7 @@ "target_label": "TY2026-27 ird taxable income people by band $123,001 - $124,000 (parked)" }, "filter": "taxable_income_band_row_133_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_134", @@ -32149,14 +32654,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -32171,7 +32680,7 @@ "target_label": "TY2026-27 ird taxable income people by band $124,001 - $125,000 (parked)" }, "filter": "taxable_income_band_row_134_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_135", @@ -32201,14 +32710,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -32223,7 +32736,7 @@ "target_label": "TY2026-27 ird taxable income people by band $125,001 - $126,000 (parked)" }, "filter": "taxable_income_band_row_135_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_136", @@ -32253,14 +32766,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -32275,7 +32792,7 @@ "target_label": "TY2026-27 ird taxable income people by band $126,001 - $127,000 (parked)" }, "filter": "taxable_income_band_row_136_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_137", @@ -32305,14 +32822,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -32327,7 +32848,7 @@ "target_label": "TY2026-27 ird taxable income people by band $127,001 - $128,000 (parked)" }, "filter": "taxable_income_band_row_137_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_138", @@ -32357,14 +32878,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -32379,7 +32904,7 @@ "target_label": "TY2026-27 ird taxable income people by band $128,001 - $129,000 (parked)" }, "filter": "taxable_income_band_row_138_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_139", @@ -32409,14 +32934,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -32431,7 +32960,7 @@ "target_label": "TY2026-27 ird taxable income people by band $129,001 - $130,000 (parked)" }, "filter": "taxable_income_band_row_139_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_140", @@ -32461,14 +32990,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -32483,7 +33016,7 @@ "target_label": "TY2026-27 ird taxable income people by band $130,001 - $131,000 (parked)" }, "filter": "taxable_income_band_row_140_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_141", @@ -32513,14 +33046,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -32535,7 +33072,7 @@ "target_label": "TY2026-27 ird taxable income people by band $131,001 - $132,000 (parked)" }, "filter": "taxable_income_band_row_141_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_142", @@ -32565,14 +33102,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -32587,7 +33128,7 @@ "target_label": "TY2026-27 ird taxable income people by band $132,001 - $133,000 (parked)" }, "filter": "taxable_income_band_row_142_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_143", @@ -32617,14 +33158,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -32639,7 +33184,7 @@ "target_label": "TY2026-27 ird taxable income people by band $133,001 - $134,000 (parked)" }, "filter": "taxable_income_band_row_143_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_144", @@ -32669,14 +33214,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -32691,7 +33240,7 @@ "target_label": "TY2026-27 ird taxable income people by band $134,001 - $135,000 (parked)" }, "filter": "taxable_income_band_row_144_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_145", @@ -32721,14 +33270,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -32743,7 +33296,7 @@ "target_label": "TY2026-27 ird taxable income people by band $135,001 - $136,000 (parked)" }, "filter": "taxable_income_band_row_145_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_146", @@ -32773,14 +33326,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -32795,7 +33352,7 @@ "target_label": "TY2026-27 ird taxable income people by band $136,001 - $137,000 (parked)" }, "filter": "taxable_income_band_row_146_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_147", @@ -32825,14 +33382,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -32847,7 +33408,7 @@ "target_label": "TY2026-27 ird taxable income people by band $137,001 - $138,000 (parked)" }, "filter": "taxable_income_band_row_147_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_148", @@ -32877,14 +33438,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -32899,7 +33464,7 @@ "target_label": "TY2026-27 ird taxable income people by band $138,001 - $139,000 (parked)" }, "filter": "taxable_income_band_row_148_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_149", @@ -32929,14 +33494,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -32951,7 +33520,7 @@ "target_label": "TY2026-27 ird taxable income people by band $139,001 - $140,000 (parked)" }, "filter": "taxable_income_band_row_149_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_150", @@ -32981,14 +33550,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -33003,7 +33576,7 @@ "target_label": "TY2026-27 ird taxable income people by band $140,001 - $141,000 (parked)" }, "filter": "taxable_income_band_row_150_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_151", @@ -33033,14 +33606,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -33055,7 +33632,7 @@ "target_label": "TY2026-27 ird taxable income people by band $141,001 - $142,000 (parked)" }, "filter": "taxable_income_band_row_151_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_152", @@ -33085,14 +33662,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -33107,7 +33688,7 @@ "target_label": "TY2026-27 ird taxable income people by band $142,001 - $143,000 (parked)" }, "filter": "taxable_income_band_row_152_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_153", @@ -33137,14 +33718,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -33159,7 +33744,7 @@ "target_label": "TY2026-27 ird taxable income people by band $143,001 - $144,000 (parked)" }, "filter": "taxable_income_band_row_153_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_154", @@ -33189,14 +33774,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -33211,7 +33800,7 @@ "target_label": "TY2026-27 ird taxable income people by band $144,001 - $145,000 (parked)" }, "filter": "taxable_income_band_row_154_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_155", @@ -33241,14 +33830,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -33263,7 +33856,7 @@ "target_label": "TY2026-27 ird taxable income people by band $145,001 - $146,000 (parked)" }, "filter": "taxable_income_band_row_155_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_156", @@ -33293,14 +33886,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -33315,7 +33912,7 @@ "target_label": "TY2026-27 ird taxable income people by band $146,001 - $147,000 (parked)" }, "filter": "taxable_income_band_row_156_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_157", @@ -33345,14 +33942,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -33367,7 +33968,7 @@ "target_label": "TY2026-27 ird taxable income people by band $147,001 - $148,000 (parked)" }, "filter": "taxable_income_band_row_157_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_158", @@ -33397,14 +33998,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -33419,7 +34024,7 @@ "target_label": "TY2026-27 ird taxable income people by band $148,001 - $149,000 (parked)" }, "filter": "taxable_income_band_row_158_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_159", @@ -33449,14 +34054,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -33471,7 +34080,7 @@ "target_label": "TY2026-27 ird taxable income people by band $149,001 - $150,000 (parked)" }, "filter": "taxable_income_band_row_159_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_160", @@ -33501,14 +34110,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -33523,7 +34136,7 @@ "target_label": "TY2026-27 ird taxable income people by band $150,001 - $151,000 (parked)" }, "filter": "taxable_income_band_row_160_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_161", @@ -33553,14 +34166,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -33575,7 +34192,7 @@ "target_label": "TY2026-27 ird taxable income people by band $151,001 - $152,000 (parked)" }, "filter": "taxable_income_band_row_161_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_162", @@ -33605,14 +34222,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -33627,7 +34248,7 @@ "target_label": "TY2026-27 ird taxable income people by band $152,001 - $153,000 (parked)" }, "filter": "taxable_income_band_row_162_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_163", @@ -33657,14 +34278,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -33679,7 +34304,7 @@ "target_label": "TY2026-27 ird taxable income people by band $153,001 - $154,000 (parked)" }, "filter": "taxable_income_band_row_163_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_164", @@ -33709,14 +34334,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -33731,7 +34360,7 @@ "target_label": "TY2026-27 ird taxable income people by band $154,001 - $155,000 (parked)" }, "filter": "taxable_income_band_row_164_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_165", @@ -33761,14 +34390,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -33783,7 +34416,7 @@ "target_label": "TY2026-27 ird taxable income people by band $155,001 - $156,000 (parked)" }, "filter": "taxable_income_band_row_165_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_166", @@ -33813,14 +34446,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -33835,7 +34472,7 @@ "target_label": "TY2026-27 ird taxable income people by band $156,001 - $157,000 (parked)" }, "filter": "taxable_income_band_row_166_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_167", @@ -33865,14 +34502,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -33887,7 +34528,7 @@ "target_label": "TY2026-27 ird taxable income people by band $157,001 - $158,000 (parked)" }, "filter": "taxable_income_band_row_167_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_168", @@ -33917,14 +34558,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -33939,7 +34584,7 @@ "target_label": "TY2026-27 ird taxable income people by band $158,001 - $159,000 (parked)" }, "filter": "taxable_income_band_row_168_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_169", @@ -33969,14 +34614,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -33991,7 +34640,7 @@ "target_label": "TY2026-27 ird taxable income people by band $159,001 - $160,000 (parked)" }, "filter": "taxable_income_band_row_169_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_170", @@ -34021,14 +34670,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -34043,7 +34696,7 @@ "target_label": "TY2026-27 ird taxable income people by band $160,001 - $161,000 (parked)" }, "filter": "taxable_income_band_row_170_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_171", @@ -34073,14 +34726,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -34095,7 +34752,7 @@ "target_label": "TY2026-27 ird taxable income people by band $161,001 - $162,000 (parked)" }, "filter": "taxable_income_band_row_171_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_172", @@ -34125,14 +34782,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -34147,7 +34808,7 @@ "target_label": "TY2026-27 ird taxable income people by band $162,001 - $163,000 (parked)" }, "filter": "taxable_income_band_row_172_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_173", @@ -34177,14 +34838,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -34199,7 +34864,7 @@ "target_label": "TY2026-27 ird taxable income people by band $163,001 - $164,000 (parked)" }, "filter": "taxable_income_band_row_173_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_174", @@ -34229,14 +34894,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -34251,7 +34920,7 @@ "target_label": "TY2026-27 ird taxable income people by band $164,001 - $165,000 (parked)" }, "filter": "taxable_income_band_row_174_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_175", @@ -34281,14 +34950,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -34303,7 +34976,7 @@ "target_label": "TY2026-27 ird taxable income people by band $165,001 - $166,000 (parked)" }, "filter": "taxable_income_band_row_175_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_176", @@ -34333,14 +35006,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -34355,7 +35032,7 @@ "target_label": "TY2026-27 ird taxable income people by band $166,001 - $167,000 (parked)" }, "filter": "taxable_income_band_row_176_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_177", @@ -34385,14 +35062,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -34407,7 +35088,7 @@ "target_label": "TY2026-27 ird taxable income people by band $167,001 - $168,000 (parked)" }, "filter": "taxable_income_band_row_177_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_178", @@ -34437,14 +35118,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -34459,7 +35144,7 @@ "target_label": "TY2026-27 ird taxable income people by band $168,001 - $169,000 (parked)" }, "filter": "taxable_income_band_row_178_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_179", @@ -34489,14 +35174,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -34511,7 +35200,7 @@ "target_label": "TY2026-27 ird taxable income people by band $169,001 - $170,000 (parked)" }, "filter": "taxable_income_band_row_179_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_180", @@ -34541,14 +35230,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -34563,7 +35256,7 @@ "target_label": "TY2026-27 ird taxable income people by band $170,001 - $171,000 (parked)" }, "filter": "taxable_income_band_row_180_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_181", @@ -34593,14 +35286,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -34615,7 +35312,7 @@ "target_label": "TY2026-27 ird taxable income people by band $171,001 - $172,000 (parked)" }, "filter": "taxable_income_band_row_181_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_182", @@ -34645,14 +35342,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -34667,7 +35368,7 @@ "target_label": "TY2026-27 ird taxable income people by band $172,001 - $173,000 (parked)" }, "filter": "taxable_income_band_row_182_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_183", @@ -34697,14 +35398,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -34719,7 +35424,7 @@ "target_label": "TY2026-27 ird taxable income people by band $173,001 - $174,000 (parked)" }, "filter": "taxable_income_band_row_183_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_184", @@ -34749,14 +35454,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -34771,7 +35480,7 @@ "target_label": "TY2026-27 ird taxable income people by band $174,001 - $175,000 (parked)" }, "filter": "taxable_income_band_row_184_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_185", @@ -34801,14 +35510,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -34823,7 +35536,7 @@ "target_label": "TY2026-27 ird taxable income people by band $175,001 - $176,000 (parked)" }, "filter": "taxable_income_band_row_185_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_186", @@ -34853,14 +35566,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -34875,7 +35592,7 @@ "target_label": "TY2026-27 ird taxable income people by band $176,001 - $177,000 (parked)" }, "filter": "taxable_income_band_row_186_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_187", @@ -34905,14 +35622,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -34927,7 +35648,7 @@ "target_label": "TY2026-27 ird taxable income people by band $177,001 - $178,000 (parked)" }, "filter": "taxable_income_band_row_187_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_188", @@ -34957,14 +35678,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -34979,7 +35704,7 @@ "target_label": "TY2026-27 ird taxable income people by band $178,001 - $179,000 (parked)" }, "filter": "taxable_income_band_row_188_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_189", @@ -35009,14 +35734,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -35031,7 +35760,7 @@ "target_label": "TY2026-27 ird taxable income people by band $179,001 - $180,000 (parked)" }, "filter": "taxable_income_band_row_189_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_190", @@ -35061,14 +35790,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -35083,7 +35816,7 @@ "target_label": "TY2026-27 ird taxable income people by band $180,001 - $181,000 (parked)" }, "filter": "taxable_income_band_row_190_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_191", @@ -35113,14 +35846,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -35135,7 +35872,7 @@ "target_label": "TY2026-27 ird taxable income people by band $181,001 - $182,000 (parked)" }, "filter": "taxable_income_band_row_191_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_192", @@ -35165,14 +35902,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -35187,7 +35928,7 @@ "target_label": "TY2026-27 ird taxable income people by band $182,001 - $183,000 (parked)" }, "filter": "taxable_income_band_row_192_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_193", @@ -35217,14 +35958,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -35239,7 +35984,7 @@ "target_label": "TY2026-27 ird taxable income people by band $183,001 - $184,000 (parked)" }, "filter": "taxable_income_band_row_193_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_194", @@ -35269,14 +36014,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -35291,7 +36040,7 @@ "target_label": "TY2026-27 ird taxable income people by band $184,001 - $185,000 (parked)" }, "filter": "taxable_income_band_row_194_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_195", @@ -35321,14 +36070,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -35343,7 +36096,7 @@ "target_label": "TY2026-27 ird taxable income people by band $185,001 - $186,000 (parked)" }, "filter": "taxable_income_band_row_195_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_196", @@ -35373,14 +36126,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -35395,7 +36152,7 @@ "target_label": "TY2026-27 ird taxable income people by band $186,001 - $187,000 (parked)" }, "filter": "taxable_income_band_row_196_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_197", @@ -35425,14 +36182,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -35447,7 +36208,7 @@ "target_label": "TY2026-27 ird taxable income people by band $187,001 - $188,000 (parked)" }, "filter": "taxable_income_band_row_197_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_198", @@ -35477,14 +36238,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -35499,7 +36264,7 @@ "target_label": "TY2026-27 ird taxable income people by band $188,001 - $189,000 (parked)" }, "filter": "taxable_income_band_row_198_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_199", @@ -35529,14 +36294,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -35551,7 +36320,7 @@ "target_label": "TY2026-27 ird taxable income people by band $189,001 - $190,000 (parked)" }, "filter": "taxable_income_band_row_199_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_200", @@ -35581,14 +36350,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -35603,7 +36376,7 @@ "target_label": "TY2026-27 ird taxable income people by band $190,001 - $191,000 (parked)" }, "filter": "taxable_income_band_row_200_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_201", @@ -35633,14 +36406,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -35655,7 +36432,7 @@ "target_label": "TY2026-27 ird taxable income people by band $191,001 - $192,000 (parked)" }, "filter": "taxable_income_band_row_201_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_202", @@ -35685,14 +36462,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -35707,7 +36488,7 @@ "target_label": "TY2026-27 ird taxable income people by band $192,001 - $193,000 (parked)" }, "filter": "taxable_income_band_row_202_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_203", @@ -35737,14 +36518,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -35759,7 +36544,7 @@ "target_label": "TY2026-27 ird taxable income people by band $193,001 - $194,000 (parked)" }, "filter": "taxable_income_band_row_203_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_204", @@ -35789,14 +36574,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -35811,7 +36600,7 @@ "target_label": "TY2026-27 ird taxable income people by band $194,001 - $195,000 (parked)" }, "filter": "taxable_income_band_row_204_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_205", @@ -35841,14 +36630,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -35863,7 +36656,7 @@ "target_label": "TY2026-27 ird taxable income people by band $195,001 - $196,000 (parked)" }, "filter": "taxable_income_band_row_205_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_206", @@ -35893,14 +36686,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -35915,7 +36712,7 @@ "target_label": "TY2026-27 ird taxable income people by band $196,001 - $197,000 (parked)" }, "filter": "taxable_income_band_row_206_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_207", @@ -35945,14 +36742,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -35967,7 +36768,7 @@ "target_label": "TY2026-27 ird taxable income people by band $197,001 - $198,000 (parked)" }, "filter": "taxable_income_band_row_207_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_208", @@ -35997,14 +36798,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -36019,7 +36824,7 @@ "target_label": "TY2026-27 ird taxable income people by band $198,001 - $199,000 (parked)" }, "filter": "taxable_income_band_row_208_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_209", @@ -36049,14 +36854,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -36071,7 +36880,7 @@ "target_label": "TY2026-27 ird taxable income people by band $199,001 - $200,000 (parked)" }, "filter": "taxable_income_band_row_209_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_210", @@ -36101,14 +36910,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -36123,7 +36936,7 @@ "target_label": "TY2026-27 ird taxable income people by band $200,001 - $205,000 (parked)" }, "filter": "taxable_income_band_row_210_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_211", @@ -36153,14 +36966,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -36175,7 +36992,7 @@ "target_label": "TY2026-27 ird taxable income people by band $205,001 - $210,000 (parked)" }, "filter": "taxable_income_band_row_211_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_212", @@ -36205,14 +37022,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -36227,7 +37048,7 @@ "target_label": "TY2026-27 ird taxable income people by band $210,001 - $215,000 (parked)" }, "filter": "taxable_income_band_row_212_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_213", @@ -36257,14 +37078,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -36279,7 +37104,7 @@ "target_label": "TY2026-27 ird taxable income people by band $215,001 - $220,000 (parked)" }, "filter": "taxable_income_band_row_213_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_214", @@ -36309,14 +37134,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -36331,7 +37160,7 @@ "target_label": "TY2026-27 ird taxable income people by band $220,001 - $225,000 (parked)" }, "filter": "taxable_income_band_row_214_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_215", @@ -36361,14 +37190,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -36383,7 +37216,7 @@ "target_label": "TY2026-27 ird taxable income people by band $225,001 - $230,000 (parked)" }, "filter": "taxable_income_band_row_215_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_216", @@ -36413,14 +37246,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -36435,7 +37272,7 @@ "target_label": "TY2026-27 ird taxable income people by band $230,001 - $235,000 (parked)" }, "filter": "taxable_income_band_row_216_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_217", @@ -36465,14 +37302,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -36487,7 +37328,7 @@ "target_label": "TY2026-27 ird taxable income people by band $235,001 - $240,000 (parked)" }, "filter": "taxable_income_band_row_217_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_218", @@ -36517,14 +37358,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -36539,7 +37384,7 @@ "target_label": "TY2026-27 ird taxable income people by band $240,001 - $245,000 (parked)" }, "filter": "taxable_income_band_row_218_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_219", @@ -36569,14 +37414,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -36591,7 +37440,7 @@ "target_label": "TY2026-27 ird taxable income people by band $245,001 - $250,000 (parked)" }, "filter": "taxable_income_band_row_219_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_220", @@ -36621,14 +37470,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -36643,7 +37496,7 @@ "target_label": "TY2026-27 ird taxable income people by band $250,001 - $255,000 (parked)" }, "filter": "taxable_income_band_row_220_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_221", @@ -36673,14 +37526,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -36695,7 +37552,7 @@ "target_label": "TY2026-27 ird taxable income people by band $255,001 - $260,000 (parked)" }, "filter": "taxable_income_band_row_221_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_222", @@ -36725,14 +37582,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -36747,7 +37608,7 @@ "target_label": "TY2026-27 ird taxable income people by band $260,001 - $265,000 (parked)" }, "filter": "taxable_income_band_row_222_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_223", @@ -36777,14 +37638,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -36799,7 +37664,7 @@ "target_label": "TY2026-27 ird taxable income people by band $265,001 - $270,000 (parked)" }, "filter": "taxable_income_band_row_223_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_224", @@ -36829,14 +37694,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -36851,7 +37720,7 @@ "target_label": "TY2026-27 ird taxable income people by band $270,001 - $275,000 (parked)" }, "filter": "taxable_income_band_row_224_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_225", @@ -36881,14 +37750,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -36903,7 +37776,7 @@ "target_label": "TY2026-27 ird taxable income people by band $275,001 - $280,000 (parked)" }, "filter": "taxable_income_band_row_225_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_226", @@ -36933,14 +37806,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -36955,7 +37832,7 @@ "target_label": "TY2026-27 ird taxable income people by band $280,001 - $285,000 (parked)" }, "filter": "taxable_income_band_row_226_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_227", @@ -36985,14 +37862,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -37007,7 +37888,7 @@ "target_label": "TY2026-27 ird taxable income people by band $285,001 - $290,000 (parked)" }, "filter": "taxable_income_band_row_227_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_228", @@ -37037,14 +37918,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -37059,7 +37944,7 @@ "target_label": "TY2026-27 ird taxable income people by band $290,001 - $295,000 (parked)" }, "filter": "taxable_income_band_row_228_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_229", @@ -37089,14 +37974,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -37111,7 +38000,7 @@ "target_label": "TY2026-27 ird taxable income people by band $295,001 - $300,000 (parked)" }, "filter": "taxable_income_band_row_229_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_230", @@ -37141,14 +38030,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -37163,7 +38056,7 @@ "target_label": "TY2026-27 ird taxable income people by band $300,000 - $350,000 (parked)" }, "filter": "taxable_income_band_row_230_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_231", @@ -37193,14 +38086,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -37215,7 +38112,7 @@ "target_label": "TY2026-27 ird taxable income people by band $350,001 - $400,000 (parked)" }, "filter": "taxable_income_band_row_231_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_232", @@ -37245,14 +38142,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -37267,7 +38168,7 @@ "target_label": "TY2026-27 ird taxable income people by band $400,001 - $450,000 (parked)" }, "filter": "taxable_income_band_row_232_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_233", @@ -37297,14 +38198,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -37319,7 +38224,7 @@ "target_label": "TY2026-27 ird taxable income people by band $450,001 - $500,000 (parked)" }, "filter": "taxable_income_band_row_233_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_234", @@ -37349,14 +38254,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -37371,7 +38280,7 @@ "target_label": "TY2026-27 ird taxable income people by band $500,001 - $550,000 (parked)" }, "filter": "taxable_income_band_row_234_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_235", @@ -37401,14 +38310,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -37423,7 +38336,7 @@ "target_label": "TY2026-27 ird taxable income people by band $550,001 - $600,000 (parked)" }, "filter": "taxable_income_band_row_235_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_236", @@ -37453,14 +38366,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -37475,7 +38392,7 @@ "target_label": "TY2026-27 ird taxable income people by band $600,001 - $650,000 (parked)" }, "filter": "taxable_income_band_row_236_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_237", @@ -37505,14 +38422,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -37527,7 +38448,7 @@ "target_label": "TY2026-27 ird taxable income people by band $650,001 - $700,000 (parked)" }, "filter": "taxable_income_band_row_237_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_238", @@ -37557,14 +38478,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -37579,7 +38504,7 @@ "target_label": "TY2026-27 ird taxable income people by band $700,001 - $750,000 (parked)" }, "filter": "taxable_income_band_row_238_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_239", @@ -37609,14 +38534,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -37631,7 +38560,7 @@ "target_label": "TY2026-27 ird taxable income people by band $750,001 - $800,000 (parked)" }, "filter": "taxable_income_band_row_239_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_240", @@ -37661,14 +38590,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -37683,7 +38616,7 @@ "target_label": "TY2026-27 ird taxable income people by band $800,001 - $850,000 (parked)" }, "filter": "taxable_income_band_row_240_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_241", @@ -37713,14 +38646,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -37735,7 +38672,7 @@ "target_label": "TY2026-27 ird taxable income people by band $850,001 - $900,000 (parked)" }, "filter": "taxable_income_band_row_241_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_242", @@ -37765,14 +38702,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -37787,7 +38728,7 @@ "target_label": "TY2026-27 ird taxable income people by band $900,001 - $950,000 (parked)" }, "filter": "taxable_income_band_row_242_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_243", @@ -37817,14 +38758,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -37839,7 +38784,7 @@ "target_label": "TY2026-27 ird taxable income people by band $950,001 - $1,000,000 (parked)" }, "filter": "taxable_income_band_row_243_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_people_by_band__row_244", @@ -37869,14 +38814,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_people_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "count" }, "hierarchy": { "provider": { @@ -37891,7 +38840,7 @@ "target_label": "TY2026-27 ird taxable income people by band Over $1,000,000 (parked)" }, "filter": "taxable_income_band_row_244_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_8", @@ -37921,14 +38870,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -37943,7 +38896,7 @@ "target_label": "TY2026-27 ird taxable income total by band nil (parked)" }, "filter": "taxable_income_band_row_8_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_9", @@ -37973,14 +38926,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -37995,7 +38952,7 @@ "target_label": "TY2026-27 ird taxable income total by band $0.01 - $100 (parked)" }, "filter": "taxable_income_band_row_9_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_10", @@ -38025,14 +38982,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -38047,7 +39008,7 @@ "target_label": "TY2026-27 ird taxable income total by band $101 - $1,000 (parked)" }, "filter": "taxable_income_band_row_10_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_11", @@ -38077,14 +39038,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -38099,7 +39064,7 @@ "target_label": "TY2026-27 ird taxable income total by band $1,001 - $2,000 (parked)" }, "filter": "taxable_income_band_row_11_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_12", @@ -38129,14 +39094,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -38151,7 +39120,7 @@ "target_label": "TY2026-27 ird taxable income total by band $2,001 - $3,000 (parked)" }, "filter": "taxable_income_band_row_12_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_13", @@ -38181,14 +39150,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -38203,7 +39176,7 @@ "target_label": "TY2026-27 ird taxable income total by band $3,001 - $4,000 (parked)" }, "filter": "taxable_income_band_row_13_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_14", @@ -38233,14 +39206,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -38255,7 +39232,7 @@ "target_label": "TY2026-27 ird taxable income total by band $4,001 - $5,000 (parked)" }, "filter": "taxable_income_band_row_14_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_15", @@ -38285,14 +39262,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -38307,7 +39288,7 @@ "target_label": "TY2026-27 ird taxable income total by band $5,001 - $6,000 (parked)" }, "filter": "taxable_income_band_row_15_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_16", @@ -38337,14 +39318,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -38359,7 +39344,7 @@ "target_label": "TY2026-27 ird taxable income total by band $6,001 - $7,000 (parked)" }, "filter": "taxable_income_band_row_16_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_17", @@ -38389,14 +39374,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -38411,7 +39400,7 @@ "target_label": "TY2026-27 ird taxable income total by band $7,001 - $8,000 (parked)" }, "filter": "taxable_income_band_row_17_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_18", @@ -38441,14 +39430,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -38463,7 +39456,7 @@ "target_label": "TY2026-27 ird taxable income total by band $8,001 - $9,000 (parked)" }, "filter": "taxable_income_band_row_18_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_19", @@ -38493,14 +39486,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -38515,7 +39512,7 @@ "target_label": "TY2026-27 ird taxable income total by band $9,001 - $10,000 (parked)" }, "filter": "taxable_income_band_row_19_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_20", @@ -38545,14 +39542,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -38567,7 +39568,7 @@ "target_label": "TY2026-27 ird taxable income total by band $10,001 - $11,000 (parked)" }, "filter": "taxable_income_band_row_20_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_21", @@ -38597,14 +39598,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -38619,7 +39624,7 @@ "target_label": "TY2026-27 ird taxable income total by band $11,001 - $12,000 (parked)" }, "filter": "taxable_income_band_row_21_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_22", @@ -38649,14 +39654,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -38671,7 +39680,7 @@ "target_label": "TY2026-27 ird taxable income total by band $12,001 - $13,000 (parked)" }, "filter": "taxable_income_band_row_22_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_23", @@ -38701,14 +39710,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -38723,7 +39736,7 @@ "target_label": "TY2026-27 ird taxable income total by band $13,001 - $14,000 (parked)" }, "filter": "taxable_income_band_row_23_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_24", @@ -38753,14 +39766,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -38775,7 +39792,7 @@ "target_label": "TY2026-27 ird taxable income total by band $14,001 - $15,000 (parked)" }, "filter": "taxable_income_band_row_24_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_25", @@ -38805,14 +39822,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -38827,7 +39848,7 @@ "target_label": "TY2026-27 ird taxable income total by band $15,001 - $16,000 (parked)" }, "filter": "taxable_income_band_row_25_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_26", @@ -38857,14 +39878,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -38879,7 +39904,7 @@ "target_label": "TY2026-27 ird taxable income total by band $16,001 - $17,000 (parked)" }, "filter": "taxable_income_band_row_26_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_27", @@ -38909,14 +39934,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -38931,7 +39960,7 @@ "target_label": "TY2026-27 ird taxable income total by band $17,001 - $18,000 (parked)" }, "filter": "taxable_income_band_row_27_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_28", @@ -38961,14 +39990,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -38983,7 +40016,7 @@ "target_label": "TY2026-27 ird taxable income total by band $18,001 - $19,000 (parked)" }, "filter": "taxable_income_band_row_28_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_29", @@ -39013,14 +40046,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -39035,7 +40072,7 @@ "target_label": "TY2026-27 ird taxable income total by band $19,001 - $20,000 (parked)" }, "filter": "taxable_income_band_row_29_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_30", @@ -39065,14 +40102,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -39087,7 +40128,7 @@ "target_label": "TY2026-27 ird taxable income total by band $20,000 - $21,000 (parked)" }, "filter": "taxable_income_band_row_30_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_31", @@ -39117,14 +40158,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -39139,7 +40184,7 @@ "target_label": "TY2026-27 ird taxable income total by band $21,001 - $22,000 (parked)" }, "filter": "taxable_income_band_row_31_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_32", @@ -39169,14 +40214,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -39191,7 +40240,7 @@ "target_label": "TY2026-27 ird taxable income total by band $22,001 - $23,000 (parked)" }, "filter": "taxable_income_band_row_32_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_33", @@ -39221,14 +40270,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -39243,7 +40296,7 @@ "target_label": "TY2026-27 ird taxable income total by band $23,001 - $24,000 (parked)" }, "filter": "taxable_income_band_row_33_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_34", @@ -39273,14 +40326,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -39295,7 +40352,7 @@ "target_label": "TY2026-27 ird taxable income total by band $24,001 - $25,000 (parked)" }, "filter": "taxable_income_band_row_34_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_35", @@ -39325,14 +40382,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -39347,7 +40408,7 @@ "target_label": "TY2026-27 ird taxable income total by band $25,001 - $26,000 (parked)" }, "filter": "taxable_income_band_row_35_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_36", @@ -39377,14 +40438,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -39399,7 +40464,7 @@ "target_label": "TY2026-27 ird taxable income total by band $26,001 - $27,000 (parked)" }, "filter": "taxable_income_band_row_36_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_37", @@ -39429,14 +40494,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -39451,7 +40520,7 @@ "target_label": "TY2026-27 ird taxable income total by band $27,001 - $28,000 (parked)" }, "filter": "taxable_income_band_row_37_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_38", @@ -39481,14 +40550,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -39503,7 +40576,7 @@ "target_label": "TY2026-27 ird taxable income total by band $28,001 - $29,000 (parked)" }, "filter": "taxable_income_band_row_38_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_39", @@ -39533,14 +40606,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -39555,7 +40632,7 @@ "target_label": "TY2026-27 ird taxable income total by band $29,001 - $30,000 (parked)" }, "filter": "taxable_income_band_row_39_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_40", @@ -39585,14 +40662,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -39607,7 +40688,7 @@ "target_label": "TY2026-27 ird taxable income total by band $30,001 - $31,000 (parked)" }, "filter": "taxable_income_band_row_40_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_41", @@ -39637,14 +40718,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -39659,7 +40744,7 @@ "target_label": "TY2026-27 ird taxable income total by band $31,001 - $32,000 (parked)" }, "filter": "taxable_income_band_row_41_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_42", @@ -39689,14 +40774,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -39711,7 +40800,7 @@ "target_label": "TY2026-27 ird taxable income total by band $32,001 - $33,000 (parked)" }, "filter": "taxable_income_band_row_42_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_43", @@ -39741,14 +40830,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -39763,7 +40856,7 @@ "target_label": "TY2026-27 ird taxable income total by band $33,001 - $34,000 (parked)" }, "filter": "taxable_income_band_row_43_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_44", @@ -39793,14 +40886,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -39815,7 +40912,7 @@ "target_label": "TY2026-27 ird taxable income total by band $34,001 - $35,000 (parked)" }, "filter": "taxable_income_band_row_44_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_45", @@ -39845,14 +40942,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -39867,7 +40968,7 @@ "target_label": "TY2026-27 ird taxable income total by band $35,001 - $36,000 (parked)" }, "filter": "taxable_income_band_row_45_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_46", @@ -39897,14 +40998,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -39919,7 +41024,7 @@ "target_label": "TY2026-27 ird taxable income total by band $36,001 - $37,000 (parked)" }, "filter": "taxable_income_band_row_46_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_47", @@ -39949,14 +41054,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -39971,7 +41080,7 @@ "target_label": "TY2026-27 ird taxable income total by band $37,001 - $38,000 (parked)" }, "filter": "taxable_income_band_row_47_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_48", @@ -40001,14 +41110,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -40023,7 +41136,7 @@ "target_label": "TY2026-27 ird taxable income total by band $38,001 - $39,000 (parked)" }, "filter": "taxable_income_band_row_48_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_49", @@ -40053,14 +41166,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -40075,7 +41192,7 @@ "target_label": "TY2026-27 ird taxable income total by band $39,001 - $40,000 (parked)" }, "filter": "taxable_income_band_row_49_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_50", @@ -40105,14 +41222,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -40127,7 +41248,7 @@ "target_label": "TY2026-27 ird taxable income total by band $40,001 - $41,000 (parked)" }, "filter": "taxable_income_band_row_50_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_51", @@ -40157,14 +41278,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -40179,7 +41304,7 @@ "target_label": "TY2026-27 ird taxable income total by band $41,001 - $42,000 (parked)" }, "filter": "taxable_income_band_row_51_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_52", @@ -40209,14 +41334,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -40231,7 +41360,7 @@ "target_label": "TY2026-27 ird taxable income total by band $42,001 - $43,000 (parked)" }, "filter": "taxable_income_band_row_52_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_53", @@ -40261,14 +41390,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -40283,7 +41416,7 @@ "target_label": "TY2026-27 ird taxable income total by band $43,001 - $44,000 (parked)" }, "filter": "taxable_income_band_row_53_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_54", @@ -40313,14 +41446,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -40335,7 +41472,7 @@ "target_label": "TY2026-27 ird taxable income total by band $44,001 - $45,000 (parked)" }, "filter": "taxable_income_band_row_54_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_55", @@ -40365,14 +41502,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -40387,7 +41528,7 @@ "target_label": "TY2026-27 ird taxable income total by band $45,001 - $46,000 (parked)" }, "filter": "taxable_income_band_row_55_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_56", @@ -40417,14 +41558,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -40439,7 +41584,7 @@ "target_label": "TY2026-27 ird taxable income total by band $46,001 - $47,000 (parked)" }, "filter": "taxable_income_band_row_56_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_57", @@ -40469,14 +41614,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -40491,7 +41640,7 @@ "target_label": "TY2026-27 ird taxable income total by band $47,001 - $48,000 (parked)" }, "filter": "taxable_income_band_row_57_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_58", @@ -40521,14 +41670,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -40543,7 +41696,7 @@ "target_label": "TY2026-27 ird taxable income total by band $48,001 - $49,000 (parked)" }, "filter": "taxable_income_band_row_58_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_59", @@ -40573,14 +41726,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -40595,7 +41752,7 @@ "target_label": "TY2026-27 ird taxable income total by band $49,001 - $50,000 (parked)" }, "filter": "taxable_income_band_row_59_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_60", @@ -40625,14 +41782,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -40647,7 +41808,7 @@ "target_label": "TY2026-27 ird taxable income total by band $50,000 - $51,000 (parked)" }, "filter": "taxable_income_band_row_60_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_61", @@ -40677,14 +41838,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -40699,7 +41864,7 @@ "target_label": "TY2026-27 ird taxable income total by band $51,001 - $52,000 (parked)" }, "filter": "taxable_income_band_row_61_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_62", @@ -40729,14 +41894,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -40751,7 +41920,7 @@ "target_label": "TY2026-27 ird taxable income total by band $52,001 - $53,000 (parked)" }, "filter": "taxable_income_band_row_62_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_63", @@ -40781,14 +41950,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -40803,7 +41976,7 @@ "target_label": "TY2026-27 ird taxable income total by band $53,001 - $54,000 (parked)" }, "filter": "taxable_income_band_row_63_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_64", @@ -40833,14 +42006,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -40855,7 +42032,7 @@ "target_label": "TY2026-27 ird taxable income total by band $54,001 - $55,000 (parked)" }, "filter": "taxable_income_band_row_64_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_65", @@ -40885,14 +42062,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -40907,7 +42088,7 @@ "target_label": "TY2026-27 ird taxable income total by band $55,001 - $56,000 (parked)" }, "filter": "taxable_income_band_row_65_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_66", @@ -40937,14 +42118,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -40959,7 +42144,7 @@ "target_label": "TY2026-27 ird taxable income total by band $56,001 - $57,000 (parked)" }, "filter": "taxable_income_band_row_66_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_67", @@ -40989,14 +42174,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -41011,7 +42200,7 @@ "target_label": "TY2026-27 ird taxable income total by band $57,001 - $58,000 (parked)" }, "filter": "taxable_income_band_row_67_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_68", @@ -41041,14 +42230,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -41063,7 +42256,7 @@ "target_label": "TY2026-27 ird taxable income total by band $58,001 - $59,000 (parked)" }, "filter": "taxable_income_band_row_68_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_69", @@ -41093,14 +42286,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -41115,7 +42312,7 @@ "target_label": "TY2026-27 ird taxable income total by band $59,001 - $60,000 (parked)" }, "filter": "taxable_income_band_row_69_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_70", @@ -41145,14 +42342,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -41167,7 +42368,7 @@ "target_label": "TY2026-27 ird taxable income total by band $60,001 - $61,000 (parked)" }, "filter": "taxable_income_band_row_70_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_71", @@ -41197,14 +42398,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -41219,7 +42424,7 @@ "target_label": "TY2026-27 ird taxable income total by band $61,001 - $62,000 (parked)" }, "filter": "taxable_income_band_row_71_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_72", @@ -41249,14 +42454,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -41271,7 +42480,7 @@ "target_label": "TY2026-27 ird taxable income total by band $62,001 - $63,000 (parked)" }, "filter": "taxable_income_band_row_72_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_73", @@ -41301,14 +42510,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -41323,7 +42536,7 @@ "target_label": "TY2026-27 ird taxable income total by band $63,001 - $64,000 (parked)" }, "filter": "taxable_income_band_row_73_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_74", @@ -41353,14 +42566,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -41375,7 +42592,7 @@ "target_label": "TY2026-27 ird taxable income total by band $64,001 - $65,000 (parked)" }, "filter": "taxable_income_band_row_74_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_75", @@ -41405,14 +42622,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -41427,7 +42648,7 @@ "target_label": "TY2026-27 ird taxable income total by band $65,001 - $66,000 (parked)" }, "filter": "taxable_income_band_row_75_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_76", @@ -41457,14 +42678,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -41479,7 +42704,7 @@ "target_label": "TY2026-27 ird taxable income total by band $66,001 - $67,000 (parked)" }, "filter": "taxable_income_band_row_76_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_77", @@ -41509,14 +42734,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -41531,7 +42760,7 @@ "target_label": "TY2026-27 ird taxable income total by band $67,001 - $68,000 (parked)" }, "filter": "taxable_income_band_row_77_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_78", @@ -41561,14 +42790,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -41583,7 +42816,7 @@ "target_label": "TY2026-27 ird taxable income total by band $68,001 - $69,000 (parked)" }, "filter": "taxable_income_band_row_78_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_79", @@ -41613,14 +42846,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -41635,7 +42872,7 @@ "target_label": "TY2026-27 ird taxable income total by band $69,001 - $70,000 (parked)" }, "filter": "taxable_income_band_row_79_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_80", @@ -41665,14 +42902,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -41687,7 +42928,7 @@ "target_label": "TY2026-27 ird taxable income total by band $70,001 - $71,000 (parked)" }, "filter": "taxable_income_band_row_80_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_81", @@ -41717,14 +42958,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -41739,7 +42984,7 @@ "target_label": "TY2026-27 ird taxable income total by band $71,001 - $72,000 (parked)" }, "filter": "taxable_income_band_row_81_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_82", @@ -41769,14 +43014,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -41791,7 +43040,7 @@ "target_label": "TY2026-27 ird taxable income total by band $72,001 - $73,000 (parked)" }, "filter": "taxable_income_band_row_82_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_83", @@ -41821,14 +43070,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -41843,7 +43096,7 @@ "target_label": "TY2026-27 ird taxable income total by band $73,001 - $74,000 (parked)" }, "filter": "taxable_income_band_row_83_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_84", @@ -41873,14 +43126,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -41895,7 +43152,7 @@ "target_label": "TY2026-27 ird taxable income total by band $74,001 - $75,000 (parked)" }, "filter": "taxable_income_band_row_84_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_85", @@ -41925,14 +43182,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -41947,7 +43208,7 @@ "target_label": "TY2026-27 ird taxable income total by band $75,001 - $76,000 (parked)" }, "filter": "taxable_income_band_row_85_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_86", @@ -41977,14 +43238,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -41999,7 +43264,7 @@ "target_label": "TY2026-27 ird taxable income total by band $76,001 - $77,000 (parked)" }, "filter": "taxable_income_band_row_86_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_87", @@ -42029,14 +43294,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -42051,7 +43320,7 @@ "target_label": "TY2026-27 ird taxable income total by band $77,001 - $78,000 (parked)" }, "filter": "taxable_income_band_row_87_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_88", @@ -42081,14 +43350,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -42103,7 +43376,7 @@ "target_label": "TY2026-27 ird taxable income total by band $78,001 - $79,000 (parked)" }, "filter": "taxable_income_band_row_88_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_89", @@ -42133,14 +43406,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -42155,7 +43432,7 @@ "target_label": "TY2026-27 ird taxable income total by band $79,001 - $80,000 (parked)" }, "filter": "taxable_income_band_row_89_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_90", @@ -42185,14 +43462,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -42207,7 +43488,7 @@ "target_label": "TY2026-27 ird taxable income total by band $80,000 - $81,000 (parked)" }, "filter": "taxable_income_band_row_90_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_91", @@ -42237,14 +43518,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -42259,7 +43544,7 @@ "target_label": "TY2026-27 ird taxable income total by band $81,001 - $82,000 (parked)" }, "filter": "taxable_income_band_row_91_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_92", @@ -42289,14 +43574,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -42311,7 +43600,7 @@ "target_label": "TY2026-27 ird taxable income total by band $82,001 - $83,000 (parked)" }, "filter": "taxable_income_band_row_92_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_93", @@ -42341,14 +43630,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -42363,7 +43656,7 @@ "target_label": "TY2026-27 ird taxable income total by band $83,001 - $84,000 (parked)" }, "filter": "taxable_income_band_row_93_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_94", @@ -42393,14 +43686,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -42415,7 +43712,7 @@ "target_label": "TY2026-27 ird taxable income total by band $84,001 - $85,000 (parked)" }, "filter": "taxable_income_band_row_94_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_95", @@ -42445,14 +43742,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -42467,7 +43768,7 @@ "target_label": "TY2026-27 ird taxable income total by band $85,001 - $86,000 (parked)" }, "filter": "taxable_income_band_row_95_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_96", @@ -42497,14 +43798,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -42519,7 +43824,7 @@ "target_label": "TY2026-27 ird taxable income total by band $86,001 - $87,000 (parked)" }, "filter": "taxable_income_band_row_96_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_97", @@ -42549,14 +43854,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -42571,7 +43880,7 @@ "target_label": "TY2026-27 ird taxable income total by band $87,001 - $88,000 (parked)" }, "filter": "taxable_income_band_row_97_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_98", @@ -42601,14 +43910,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -42623,7 +43936,7 @@ "target_label": "TY2026-27 ird taxable income total by band $88,001 - $89,000 (parked)" }, "filter": "taxable_income_band_row_98_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_99", @@ -42653,14 +43966,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -42675,7 +43992,7 @@ "target_label": "TY2026-27 ird taxable income total by band $89,001 - $90,000 (parked)" }, "filter": "taxable_income_band_row_99_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_100", @@ -42705,14 +44022,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -42727,7 +44048,7 @@ "target_label": "TY2026-27 ird taxable income total by band $90,001 - $91,000 (parked)" }, "filter": "taxable_income_band_row_100_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_101", @@ -42757,14 +44078,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -42779,7 +44104,7 @@ "target_label": "TY2026-27 ird taxable income total by band $91,001 - $92,000 (parked)" }, "filter": "taxable_income_band_row_101_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_102", @@ -42809,14 +44134,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -42831,7 +44160,7 @@ "target_label": "TY2026-27 ird taxable income total by band $92,001 - $93,000 (parked)" }, "filter": "taxable_income_band_row_102_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_103", @@ -42861,14 +44190,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -42883,7 +44216,7 @@ "target_label": "TY2026-27 ird taxable income total by band $93,001 - $94,000 (parked)" }, "filter": "taxable_income_band_row_103_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_104", @@ -42913,14 +44246,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -42935,7 +44272,7 @@ "target_label": "TY2026-27 ird taxable income total by band $94,001 - $95,000 (parked)" }, "filter": "taxable_income_band_row_104_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_105", @@ -42965,14 +44302,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -42987,7 +44328,7 @@ "target_label": "TY2026-27 ird taxable income total by band $95,001 - $96,000 (parked)" }, "filter": "taxable_income_band_row_105_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_106", @@ -43017,14 +44358,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -43039,7 +44384,7 @@ "target_label": "TY2026-27 ird taxable income total by band $96,001 - $97,000 (parked)" }, "filter": "taxable_income_band_row_106_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_107", @@ -43069,14 +44414,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -43091,7 +44440,7 @@ "target_label": "TY2026-27 ird taxable income total by band $97,001 - $98,000 (parked)" }, "filter": "taxable_income_band_row_107_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_108", @@ -43121,14 +44470,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -43143,7 +44496,7 @@ "target_label": "TY2026-27 ird taxable income total by band $98,001 - $99,000 (parked)" }, "filter": "taxable_income_band_row_108_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_109", @@ -43173,14 +44526,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -43195,7 +44552,7 @@ "target_label": "TY2026-27 ird taxable income total by band $99,001 - $100,000 (parked)" }, "filter": "taxable_income_band_row_109_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_110", @@ -43225,14 +44582,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -43247,7 +44608,7 @@ "target_label": "TY2026-27 ird taxable income total by band $100,001 - $101,000 (parked)" }, "filter": "taxable_income_band_row_110_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_111", @@ -43277,14 +44638,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -43299,7 +44664,7 @@ "target_label": "TY2026-27 ird taxable income total by band $101,001 - $102,000 (parked)" }, "filter": "taxable_income_band_row_111_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_112", @@ -43329,14 +44694,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -43351,7 +44720,7 @@ "target_label": "TY2026-27 ird taxable income total by band $102,001 - $103,000 (parked)" }, "filter": "taxable_income_band_row_112_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_113", @@ -43381,14 +44750,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -43403,7 +44776,7 @@ "target_label": "TY2026-27 ird taxable income total by band $103,001 - $104,000 (parked)" }, "filter": "taxable_income_band_row_113_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_114", @@ -43433,14 +44806,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -43455,7 +44832,7 @@ "target_label": "TY2026-27 ird taxable income total by band $104,001 - $105,000 (parked)" }, "filter": "taxable_income_band_row_114_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_115", @@ -43485,14 +44862,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -43507,7 +44888,7 @@ "target_label": "TY2026-27 ird taxable income total by band $105,001 - $106,000 (parked)" }, "filter": "taxable_income_band_row_115_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_116", @@ -43537,14 +44918,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -43559,7 +44944,7 @@ "target_label": "TY2026-27 ird taxable income total by band $106,001 - $107,000 (parked)" }, "filter": "taxable_income_band_row_116_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_117", @@ -43589,14 +44974,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -43611,7 +45000,7 @@ "target_label": "TY2026-27 ird taxable income total by band $107,001 - $108,000 (parked)" }, "filter": "taxable_income_band_row_117_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_118", @@ -43641,14 +45030,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -43663,7 +45056,7 @@ "target_label": "TY2026-27 ird taxable income total by band $108,001 - $109,000 (parked)" }, "filter": "taxable_income_band_row_118_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_119", @@ -43693,14 +45086,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -43715,7 +45112,7 @@ "target_label": "TY2026-27 ird taxable income total by band $109,001 - $110,000 (parked)" }, "filter": "taxable_income_band_row_119_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_120", @@ -43745,14 +45142,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -43767,7 +45168,7 @@ "target_label": "TY2026-27 ird taxable income total by band $110,001 - $111,000 (parked)" }, "filter": "taxable_income_band_row_120_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_121", @@ -43797,14 +45198,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -43819,7 +45224,7 @@ "target_label": "TY2026-27 ird taxable income total by band $111,001 - $112,000 (parked)" }, "filter": "taxable_income_band_row_121_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_122", @@ -43849,14 +45254,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -43871,7 +45280,7 @@ "target_label": "TY2026-27 ird taxable income total by band $112,001 - $113,000 (parked)" }, "filter": "taxable_income_band_row_122_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_123", @@ -43901,14 +45310,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -43923,7 +45336,7 @@ "target_label": "TY2026-27 ird taxable income total by band $113,001 - $114,000 (parked)" }, "filter": "taxable_income_band_row_123_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_124", @@ -43953,14 +45366,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -43975,7 +45392,7 @@ "target_label": "TY2026-27 ird taxable income total by band $114,001 - $115,000 (parked)" }, "filter": "taxable_income_band_row_124_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_125", @@ -44005,14 +45422,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -44027,7 +45448,7 @@ "target_label": "TY2026-27 ird taxable income total by band $115,001 - $116,000 (parked)" }, "filter": "taxable_income_band_row_125_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_126", @@ -44057,14 +45478,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -44079,7 +45504,7 @@ "target_label": "TY2026-27 ird taxable income total by band $116,001 - $117,000 (parked)" }, "filter": "taxable_income_band_row_126_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_127", @@ -44109,14 +45534,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -44131,7 +45560,7 @@ "target_label": "TY2026-27 ird taxable income total by band $117,001 - $118,000 (parked)" }, "filter": "taxable_income_band_row_127_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_128", @@ -44161,14 +45590,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -44183,7 +45616,7 @@ "target_label": "TY2026-27 ird taxable income total by band $118,001 - $119,000 (parked)" }, "filter": "taxable_income_band_row_128_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_129", @@ -44213,14 +45646,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -44235,7 +45672,7 @@ "target_label": "TY2026-27 ird taxable income total by band $119,001 - $120,000 (parked)" }, "filter": "taxable_income_band_row_129_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_130", @@ -44265,14 +45702,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -44287,7 +45728,7 @@ "target_label": "TY2026-27 ird taxable income total by band $120,001 - $121,000 (parked)" }, "filter": "taxable_income_band_row_130_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_131", @@ -44317,14 +45758,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -44339,7 +45784,7 @@ "target_label": "TY2026-27 ird taxable income total by band $121,001 - $122,000 (parked)" }, "filter": "taxable_income_band_row_131_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_132", @@ -44369,14 +45814,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -44391,7 +45840,7 @@ "target_label": "TY2026-27 ird taxable income total by band $122,001 - $123,000 (parked)" }, "filter": "taxable_income_band_row_132_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_133", @@ -44421,14 +45870,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -44443,7 +45896,7 @@ "target_label": "TY2026-27 ird taxable income total by band $123,001 - $124,000 (parked)" }, "filter": "taxable_income_band_row_133_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_134", @@ -44473,14 +45926,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -44495,7 +45952,7 @@ "target_label": "TY2026-27 ird taxable income total by band $124,001 - $125,000 (parked)" }, "filter": "taxable_income_band_row_134_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_135", @@ -44525,14 +45982,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -44547,7 +46008,7 @@ "target_label": "TY2026-27 ird taxable income total by band $125,001 - $126,000 (parked)" }, "filter": "taxable_income_band_row_135_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_136", @@ -44577,14 +46038,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -44599,7 +46064,7 @@ "target_label": "TY2026-27 ird taxable income total by band $126,001 - $127,000 (parked)" }, "filter": "taxable_income_band_row_136_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_137", @@ -44629,14 +46094,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -44651,7 +46120,7 @@ "target_label": "TY2026-27 ird taxable income total by band $127,001 - $128,000 (parked)" }, "filter": "taxable_income_band_row_137_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_138", @@ -44681,14 +46150,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -44703,7 +46176,7 @@ "target_label": "TY2026-27 ird taxable income total by band $128,001 - $129,000 (parked)" }, "filter": "taxable_income_band_row_138_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_139", @@ -44733,14 +46206,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -44755,7 +46232,7 @@ "target_label": "TY2026-27 ird taxable income total by band $129,001 - $130,000 (parked)" }, "filter": "taxable_income_band_row_139_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_140", @@ -44785,14 +46262,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -44807,7 +46288,7 @@ "target_label": "TY2026-27 ird taxable income total by band $130,001 - $131,000 (parked)" }, "filter": "taxable_income_band_row_140_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_141", @@ -44837,14 +46318,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -44859,7 +46344,7 @@ "target_label": "TY2026-27 ird taxable income total by band $131,001 - $132,000 (parked)" }, "filter": "taxable_income_band_row_141_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_142", @@ -44889,14 +46374,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -44911,7 +46400,7 @@ "target_label": "TY2026-27 ird taxable income total by band $132,001 - $133,000 (parked)" }, "filter": "taxable_income_band_row_142_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_143", @@ -44941,14 +46430,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -44963,7 +46456,7 @@ "target_label": "TY2026-27 ird taxable income total by band $133,001 - $134,000 (parked)" }, "filter": "taxable_income_band_row_143_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_144", @@ -44993,14 +46486,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -45015,7 +46512,7 @@ "target_label": "TY2026-27 ird taxable income total by band $134,001 - $135,000 (parked)" }, "filter": "taxable_income_band_row_144_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_145", @@ -45045,14 +46542,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -45067,7 +46568,7 @@ "target_label": "TY2026-27 ird taxable income total by band $135,001 - $136,000 (parked)" }, "filter": "taxable_income_band_row_145_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_146", @@ -45097,14 +46598,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -45119,7 +46624,7 @@ "target_label": "TY2026-27 ird taxable income total by band $136,001 - $137,000 (parked)" }, "filter": "taxable_income_band_row_146_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_147", @@ -45149,14 +46654,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -45171,7 +46680,7 @@ "target_label": "TY2026-27 ird taxable income total by band $137,001 - $138,000 (parked)" }, "filter": "taxable_income_band_row_147_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_148", @@ -45201,14 +46710,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -45223,7 +46736,7 @@ "target_label": "TY2026-27 ird taxable income total by band $138,001 - $139,000 (parked)" }, "filter": "taxable_income_band_row_148_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_149", @@ -45253,14 +46766,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -45275,7 +46792,7 @@ "target_label": "TY2026-27 ird taxable income total by band $139,001 - $140,000 (parked)" }, "filter": "taxable_income_band_row_149_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_150", @@ -45305,14 +46822,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -45327,7 +46848,7 @@ "target_label": "TY2026-27 ird taxable income total by band $140,001 - $141,000 (parked)" }, "filter": "taxable_income_band_row_150_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_151", @@ -45357,14 +46878,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -45379,7 +46904,7 @@ "target_label": "TY2026-27 ird taxable income total by band $141,001 - $142,000 (parked)" }, "filter": "taxable_income_band_row_151_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_152", @@ -45409,14 +46934,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -45431,7 +46960,7 @@ "target_label": "TY2026-27 ird taxable income total by band $142,001 - $143,000 (parked)" }, "filter": "taxable_income_band_row_152_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_153", @@ -45461,14 +46990,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -45483,7 +47016,7 @@ "target_label": "TY2026-27 ird taxable income total by band $143,001 - $144,000 (parked)" }, "filter": "taxable_income_band_row_153_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_154", @@ -45513,14 +47046,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -45535,7 +47072,7 @@ "target_label": "TY2026-27 ird taxable income total by band $144,001 - $145,000 (parked)" }, "filter": "taxable_income_band_row_154_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_155", @@ -45565,14 +47102,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -45587,7 +47128,7 @@ "target_label": "TY2026-27 ird taxable income total by band $145,001 - $146,000 (parked)" }, "filter": "taxable_income_band_row_155_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_156", @@ -45617,14 +47158,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -45639,7 +47184,7 @@ "target_label": "TY2026-27 ird taxable income total by band $146,001 - $147,000 (parked)" }, "filter": "taxable_income_band_row_156_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_157", @@ -45669,14 +47214,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -45691,7 +47240,7 @@ "target_label": "TY2026-27 ird taxable income total by band $147,001 - $148,000 (parked)" }, "filter": "taxable_income_band_row_157_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_158", @@ -45721,14 +47270,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -45743,7 +47296,7 @@ "target_label": "TY2026-27 ird taxable income total by band $148,001 - $149,000 (parked)" }, "filter": "taxable_income_band_row_158_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_159", @@ -45773,14 +47326,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -45795,7 +47352,7 @@ "target_label": "TY2026-27 ird taxable income total by band $149,001 - $150,000 (parked)" }, "filter": "taxable_income_band_row_159_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_160", @@ -45825,14 +47382,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -45847,7 +47408,7 @@ "target_label": "TY2026-27 ird taxable income total by band $150,001 - $151,000 (parked)" }, "filter": "taxable_income_band_row_160_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_161", @@ -45877,14 +47438,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -45899,7 +47464,7 @@ "target_label": "TY2026-27 ird taxable income total by band $151,001 - $152,000 (parked)" }, "filter": "taxable_income_band_row_161_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_162", @@ -45929,14 +47494,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -45951,7 +47520,7 @@ "target_label": "TY2026-27 ird taxable income total by band $152,001 - $153,000 (parked)" }, "filter": "taxable_income_band_row_162_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_163", @@ -45981,14 +47550,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -46003,7 +47576,7 @@ "target_label": "TY2026-27 ird taxable income total by band $153,001 - $154,000 (parked)" }, "filter": "taxable_income_band_row_163_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_164", @@ -46033,14 +47606,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -46055,7 +47632,7 @@ "target_label": "TY2026-27 ird taxable income total by band $154,001 - $155,000 (parked)" }, "filter": "taxable_income_band_row_164_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_165", @@ -46085,14 +47662,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -46107,7 +47688,7 @@ "target_label": "TY2026-27 ird taxable income total by band $155,001 - $156,000 (parked)" }, "filter": "taxable_income_band_row_165_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_166", @@ -46137,14 +47718,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -46159,7 +47744,7 @@ "target_label": "TY2026-27 ird taxable income total by band $156,001 - $157,000 (parked)" }, "filter": "taxable_income_band_row_166_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_167", @@ -46189,14 +47774,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -46211,7 +47800,7 @@ "target_label": "TY2026-27 ird taxable income total by band $157,001 - $158,000 (parked)" }, "filter": "taxable_income_band_row_167_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_168", @@ -46241,14 +47830,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -46263,7 +47856,7 @@ "target_label": "TY2026-27 ird taxable income total by band $158,001 - $159,000 (parked)" }, "filter": "taxable_income_band_row_168_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_169", @@ -46293,14 +47886,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -46315,7 +47912,7 @@ "target_label": "TY2026-27 ird taxable income total by band $159,001 - $160,000 (parked)" }, "filter": "taxable_income_band_row_169_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_170", @@ -46345,14 +47942,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -46367,7 +47968,7 @@ "target_label": "TY2026-27 ird taxable income total by band $160,001 - $161,000 (parked)" }, "filter": "taxable_income_band_row_170_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_171", @@ -46397,14 +47998,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -46419,7 +48024,7 @@ "target_label": "TY2026-27 ird taxable income total by band $161,001 - $162,000 (parked)" }, "filter": "taxable_income_band_row_171_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_172", @@ -46449,14 +48054,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -46471,7 +48080,7 @@ "target_label": "TY2026-27 ird taxable income total by band $162,001 - $163,000 (parked)" }, "filter": "taxable_income_band_row_172_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_173", @@ -46501,14 +48110,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -46523,7 +48136,7 @@ "target_label": "TY2026-27 ird taxable income total by band $163,001 - $164,000 (parked)" }, "filter": "taxable_income_band_row_173_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_174", @@ -46553,14 +48166,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -46575,7 +48192,7 @@ "target_label": "TY2026-27 ird taxable income total by band $164,001 - $165,000 (parked)" }, "filter": "taxable_income_band_row_174_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_175", @@ -46605,14 +48222,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -46627,7 +48248,7 @@ "target_label": "TY2026-27 ird taxable income total by band $165,001 - $166,000 (parked)" }, "filter": "taxable_income_band_row_175_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_176", @@ -46657,14 +48278,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -46679,7 +48304,7 @@ "target_label": "TY2026-27 ird taxable income total by band $166,001 - $167,000 (parked)" }, "filter": "taxable_income_band_row_176_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_177", @@ -46709,14 +48334,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -46731,7 +48360,7 @@ "target_label": "TY2026-27 ird taxable income total by band $167,001 - $168,000 (parked)" }, "filter": "taxable_income_band_row_177_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_178", @@ -46761,14 +48390,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -46783,7 +48416,7 @@ "target_label": "TY2026-27 ird taxable income total by band $168,001 - $169,000 (parked)" }, "filter": "taxable_income_band_row_178_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_179", @@ -46813,14 +48446,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -46835,7 +48472,7 @@ "target_label": "TY2026-27 ird taxable income total by band $169,001 - $170,000 (parked)" }, "filter": "taxable_income_band_row_179_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_180", @@ -46865,14 +48502,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -46887,7 +48528,7 @@ "target_label": "TY2026-27 ird taxable income total by band $170,001 - $171,000 (parked)" }, "filter": "taxable_income_band_row_180_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_181", @@ -46917,14 +48558,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -46939,7 +48584,7 @@ "target_label": "TY2026-27 ird taxable income total by band $171,001 - $172,000 (parked)" }, "filter": "taxable_income_band_row_181_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_182", @@ -46969,14 +48614,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -46991,7 +48640,7 @@ "target_label": "TY2026-27 ird taxable income total by band $172,001 - $173,000 (parked)" }, "filter": "taxable_income_band_row_182_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_183", @@ -47021,14 +48670,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -47043,7 +48696,7 @@ "target_label": "TY2026-27 ird taxable income total by band $173,001 - $174,000 (parked)" }, "filter": "taxable_income_band_row_183_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_184", @@ -47073,14 +48726,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -47095,7 +48752,7 @@ "target_label": "TY2026-27 ird taxable income total by band $174,001 - $175,000 (parked)" }, "filter": "taxable_income_band_row_184_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_185", @@ -47125,14 +48782,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -47147,7 +48808,7 @@ "target_label": "TY2026-27 ird taxable income total by band $175,001 - $176,000 (parked)" }, "filter": "taxable_income_band_row_185_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_186", @@ -47177,14 +48838,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -47199,7 +48864,7 @@ "target_label": "TY2026-27 ird taxable income total by band $176,001 - $177,000 (parked)" }, "filter": "taxable_income_band_row_186_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_187", @@ -47229,14 +48894,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -47251,7 +48920,7 @@ "target_label": "TY2026-27 ird taxable income total by band $177,001 - $178,000 (parked)" }, "filter": "taxable_income_band_row_187_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_188", @@ -47281,14 +48950,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -47303,7 +48976,7 @@ "target_label": "TY2026-27 ird taxable income total by band $178,001 - $179,000 (parked)" }, "filter": "taxable_income_band_row_188_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_189", @@ -47333,14 +49006,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -47355,7 +49032,7 @@ "target_label": "TY2026-27 ird taxable income total by band $179,001 - $180,000 (parked)" }, "filter": "taxable_income_band_row_189_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_190", @@ -47385,14 +49062,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -47407,7 +49088,7 @@ "target_label": "TY2026-27 ird taxable income total by band $180,001 - $181,000 (parked)" }, "filter": "taxable_income_band_row_190_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_191", @@ -47437,14 +49118,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -47459,7 +49144,7 @@ "target_label": "TY2026-27 ird taxable income total by band $181,001 - $182,000 (parked)" }, "filter": "taxable_income_band_row_191_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_192", @@ -47489,14 +49174,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -47511,7 +49200,7 @@ "target_label": "TY2026-27 ird taxable income total by band $182,001 - $183,000 (parked)" }, "filter": "taxable_income_band_row_192_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_193", @@ -47541,14 +49230,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -47563,7 +49256,7 @@ "target_label": "TY2026-27 ird taxable income total by band $183,001 - $184,000 (parked)" }, "filter": "taxable_income_band_row_193_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_194", @@ -47593,14 +49286,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -47615,7 +49312,7 @@ "target_label": "TY2026-27 ird taxable income total by band $184,001 - $185,000 (parked)" }, "filter": "taxable_income_band_row_194_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_195", @@ -47645,14 +49342,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -47667,7 +49368,7 @@ "target_label": "TY2026-27 ird taxable income total by band $185,001 - $186,000 (parked)" }, "filter": "taxable_income_band_row_195_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_196", @@ -47697,14 +49398,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -47719,7 +49424,7 @@ "target_label": "TY2026-27 ird taxable income total by band $186,001 - $187,000 (parked)" }, "filter": "taxable_income_band_row_196_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_197", @@ -47749,14 +49454,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -47771,7 +49480,7 @@ "target_label": "TY2026-27 ird taxable income total by band $187,001 - $188,000 (parked)" }, "filter": "taxable_income_band_row_197_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_198", @@ -47801,14 +49510,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -47823,7 +49536,7 @@ "target_label": "TY2026-27 ird taxable income total by band $188,001 - $189,000 (parked)" }, "filter": "taxable_income_band_row_198_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_199", @@ -47853,14 +49566,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -47875,7 +49592,7 @@ "target_label": "TY2026-27 ird taxable income total by band $189,001 - $190,000 (parked)" }, "filter": "taxable_income_band_row_199_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_200", @@ -47905,14 +49622,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -47927,7 +49648,7 @@ "target_label": "TY2026-27 ird taxable income total by band $190,001 - $191,000 (parked)" }, "filter": "taxable_income_band_row_200_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_201", @@ -47957,14 +49678,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -47979,7 +49704,7 @@ "target_label": "TY2026-27 ird taxable income total by band $191,001 - $192,000 (parked)" }, "filter": "taxable_income_band_row_201_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_202", @@ -48009,14 +49734,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -48031,7 +49760,7 @@ "target_label": "TY2026-27 ird taxable income total by band $192,001 - $193,000 (parked)" }, "filter": "taxable_income_band_row_202_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_203", @@ -48061,14 +49790,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -48083,7 +49816,7 @@ "target_label": "TY2026-27 ird taxable income total by band $193,001 - $194,000 (parked)" }, "filter": "taxable_income_band_row_203_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_204", @@ -48113,14 +49846,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -48135,7 +49872,7 @@ "target_label": "TY2026-27 ird taxable income total by band $194,001 - $195,000 (parked)" }, "filter": "taxable_income_band_row_204_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_205", @@ -48165,14 +49902,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -48187,7 +49928,7 @@ "target_label": "TY2026-27 ird taxable income total by band $195,001 - $196,000 (parked)" }, "filter": "taxable_income_band_row_205_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_206", @@ -48217,14 +49958,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -48239,7 +49984,7 @@ "target_label": "TY2026-27 ird taxable income total by band $196,001 - $197,000 (parked)" }, "filter": "taxable_income_band_row_206_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_207", @@ -48269,14 +50014,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -48291,7 +50040,7 @@ "target_label": "TY2026-27 ird taxable income total by band $197,001 - $198,000 (parked)" }, "filter": "taxable_income_band_row_207_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_208", @@ -48321,14 +50070,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -48343,7 +50096,7 @@ "target_label": "TY2026-27 ird taxable income total by band $198,001 - $199,000 (parked)" }, "filter": "taxable_income_band_row_208_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_209", @@ -48373,14 +50126,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -48395,7 +50152,7 @@ "target_label": "TY2026-27 ird taxable income total by band $199,001 - $200,000 (parked)" }, "filter": "taxable_income_band_row_209_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_210", @@ -48425,14 +50182,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -48447,7 +50208,7 @@ "target_label": "TY2026-27 ird taxable income total by band $200,001 - $205,000 (parked)" }, "filter": "taxable_income_band_row_210_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_211", @@ -48477,14 +50238,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -48499,7 +50264,7 @@ "target_label": "TY2026-27 ird taxable income total by band $205,001 - $210,000 (parked)" }, "filter": "taxable_income_band_row_211_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_212", @@ -48529,14 +50294,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -48551,7 +50320,7 @@ "target_label": "TY2026-27 ird taxable income total by band $210,001 - $215,000 (parked)" }, "filter": "taxable_income_band_row_212_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_213", @@ -48581,14 +50350,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -48603,7 +50376,7 @@ "target_label": "TY2026-27 ird taxable income total by band $215,001 - $220,000 (parked)" }, "filter": "taxable_income_band_row_213_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_214", @@ -48633,14 +50406,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -48655,7 +50432,7 @@ "target_label": "TY2026-27 ird taxable income total by band $220,001 - $225,000 (parked)" }, "filter": "taxable_income_band_row_214_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_215", @@ -48685,14 +50462,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -48707,7 +50488,7 @@ "target_label": "TY2026-27 ird taxable income total by band $225,001 - $230,000 (parked)" }, "filter": "taxable_income_band_row_215_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_216", @@ -48737,14 +50518,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -48759,7 +50544,7 @@ "target_label": "TY2026-27 ird taxable income total by band $230,001 - $235,000 (parked)" }, "filter": "taxable_income_band_row_216_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_217", @@ -48789,14 +50574,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -48811,7 +50600,7 @@ "target_label": "TY2026-27 ird taxable income total by band $235,001 - $240,000 (parked)" }, "filter": "taxable_income_band_row_217_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_218", @@ -48841,14 +50630,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -48863,7 +50656,7 @@ "target_label": "TY2026-27 ird taxable income total by band $240,001 - $245,000 (parked)" }, "filter": "taxable_income_band_row_218_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_219", @@ -48893,14 +50686,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -48915,7 +50712,7 @@ "target_label": "TY2026-27 ird taxable income total by band $245,001 - $250,000 (parked)" }, "filter": "taxable_income_band_row_219_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_220", @@ -48945,14 +50742,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -48967,7 +50768,7 @@ "target_label": "TY2026-27 ird taxable income total by band $250,001 - $255,000 (parked)" }, "filter": "taxable_income_band_row_220_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_221", @@ -48997,14 +50798,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -49019,7 +50824,7 @@ "target_label": "TY2026-27 ird taxable income total by band $255,001 - $260,000 (parked)" }, "filter": "taxable_income_band_row_221_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_222", @@ -49049,14 +50854,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -49071,7 +50880,7 @@ "target_label": "TY2026-27 ird taxable income total by band $260,001 - $265,000 (parked)" }, "filter": "taxable_income_band_row_222_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_223", @@ -49101,14 +50910,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -49123,7 +50936,7 @@ "target_label": "TY2026-27 ird taxable income total by band $265,001 - $270,000 (parked)" }, "filter": "taxable_income_band_row_223_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_224", @@ -49153,14 +50966,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -49175,7 +50992,7 @@ "target_label": "TY2026-27 ird taxable income total by band $270,001 - $275,000 (parked)" }, "filter": "taxable_income_band_row_224_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_225", @@ -49205,14 +51022,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -49227,7 +51048,7 @@ "target_label": "TY2026-27 ird taxable income total by band $275,001 - $280,000 (parked)" }, "filter": "taxable_income_band_row_225_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_226", @@ -49257,14 +51078,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -49279,7 +51104,7 @@ "target_label": "TY2026-27 ird taxable income total by band $280,001 - $285,000 (parked)" }, "filter": "taxable_income_band_row_226_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_227", @@ -49309,14 +51134,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -49331,7 +51160,7 @@ "target_label": "TY2026-27 ird taxable income total by band $285,001 - $290,000 (parked)" }, "filter": "taxable_income_band_row_227_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_228", @@ -49361,14 +51190,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -49383,7 +51216,7 @@ "target_label": "TY2026-27 ird taxable income total by band $290,001 - $295,000 (parked)" }, "filter": "taxable_income_band_row_228_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_229", @@ -49413,14 +51246,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -49435,7 +51272,7 @@ "target_label": "TY2026-27 ird taxable income total by band $295,001 - $300,000 (parked)" }, "filter": "taxable_income_band_row_229_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_230", @@ -49465,14 +51302,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -49487,7 +51328,7 @@ "target_label": "TY2026-27 ird taxable income total by band $300,000 - $350,000 (parked)" }, "filter": "taxable_income_band_row_230_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_231", @@ -49517,14 +51358,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -49539,7 +51384,7 @@ "target_label": "TY2026-27 ird taxable income total by band $350,001 - $400,000 (parked)" }, "filter": "taxable_income_band_row_231_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_232", @@ -49569,14 +51414,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -49591,7 +51440,7 @@ "target_label": "TY2026-27 ird taxable income total by band $400,001 - $450,000 (parked)" }, "filter": "taxable_income_band_row_232_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_233", @@ -49621,14 +51470,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -49643,7 +51496,7 @@ "target_label": "TY2026-27 ird taxable income total by band $450,001 - $500,000 (parked)" }, "filter": "taxable_income_band_row_233_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_234", @@ -49673,14 +51526,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -49695,7 +51552,7 @@ "target_label": "TY2026-27 ird taxable income total by band $500,001 - $550,000 (parked)" }, "filter": "taxable_income_band_row_234_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_235", @@ -49725,14 +51582,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -49747,7 +51608,7 @@ "target_label": "TY2026-27 ird taxable income total by band $550,001 - $600,000 (parked)" }, "filter": "taxable_income_band_row_235_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_236", @@ -49777,14 +51638,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -49799,7 +51664,7 @@ "target_label": "TY2026-27 ird taxable income total by band $600,001 - $650,000 (parked)" }, "filter": "taxable_income_band_row_236_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_237", @@ -49829,14 +51694,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -49851,7 +51720,7 @@ "target_label": "TY2026-27 ird taxable income total by band $650,001 - $700,000 (parked)" }, "filter": "taxable_income_band_row_237_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_238", @@ -49881,14 +51750,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -49903,7 +51776,7 @@ "target_label": "TY2026-27 ird taxable income total by band $700,001 - $750,000 (parked)" }, "filter": "taxable_income_band_row_238_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_239", @@ -49933,14 +51806,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -49955,7 +51832,7 @@ "target_label": "TY2026-27 ird taxable income total by band $750,001 - $800,000 (parked)" }, "filter": "taxable_income_band_row_239_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_240", @@ -49985,14 +51862,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -50007,7 +51888,7 @@ "target_label": "TY2026-27 ird taxable income total by band $800,001 - $850,000 (parked)" }, "filter": "taxable_income_band_row_240_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_241", @@ -50037,14 +51918,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -50059,7 +51944,7 @@ "target_label": "TY2026-27 ird taxable income total by band $850,001 - $900,000 (parked)" }, "filter": "taxable_income_band_row_241_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_242", @@ -50089,14 +51974,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -50111,7 +52000,7 @@ "target_label": "TY2026-27 ird taxable income total by band $900,001 - $950,000 (parked)" }, "filter": "taxable_income_band_row_242_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_243", @@ -50141,14 +52030,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -50163,7 +52056,7 @@ "target_label": "TY2026-27 ird taxable income total by band $950,001 - $1,000,000 (parked)" }, "filter": "taxable_income_band_row_243_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "ird_taxable_income_total_by_band__row_244", @@ -50193,14 +52086,18 @@ "assertion_policy": "observed_only", "metadata": { "contract_target_id": "ird_taxable_income_total_by_band", - "activation_status": "requires_g8r2_taxable_income_contract_intervals_and_ageing", + "activation_status": "requires_prerequisites", "criticality": "release_blocking", "measure_kind": "prepared_column", "publisher": "Inland Revenue", "target_role": "calibration", "reference_set": "calibration", "method_card_row": "MC5", - "parking_reason": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "parking_reason": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement.", + "reference_prerequisites": "[\"reference_profile:aging_index\",\"measurement:ird_taxable_income_ty2027_v1\"]", + "required_reference_profile": "aging_index", + "required_measurement_binding": "ird_taxable_income_ty2027_v1", + "reference_value_kind": "money" }, "hierarchy": { "provider": { @@ -50215,7 +52112,7 @@ "target_label": "TY2026-27 ird taxable income total by band Over $1,000,000 (parked)" }, "filter": "taxable_income_band_row_244_indicator", - "notes": "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." + "notes": "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." }, { "name": "msd_jobseeker_support_recipients__age_18_24", diff --git a/packages/microcosm-build/src/microcosm/build/reference_factors.py b/packages/microcosm-build/src/microcosm/build/reference_factors.py new file mode 100644 index 0000000000..b8ad41911f --- /dev/null +++ b/packages/microcosm-build/src/microcosm/build/reference_factors.py @@ -0,0 +1,711 @@ +"""Closed, declaration-driven factor calculations with exact feed operands. + +The operational rational is rounded once. Irrational roots remain certified +intervals in the evidence; they are never represented as exact rationals. +Composition checks declared evidence; compilation must replay it against the +facts returned by the authenticated artifact loader. +""" + +from __future__ import annotations + +import hashlib +from collections.abc import Iterable, Mapping +from datetime import date, timedelta +from decimal import Decimal, InvalidOperation +from fractions import Fraction + +from microcosm.build.ledger_artifact import LexicalFloat, chronicle_feed_identity +from microcosm.build.ledger_targets import ( + ALLOWED_ASSERTION_POLICIES, + fact_matches_selector, + selector_field_is_supported, +) +from microcosm.graph.canonical import canonical_json + +OPERATION = "projected_period_average_ratio" +OPERATION_VERSION = 1 +_DECLARATION_FIELDS = frozenset( + { + "schema_version", + "id", + "operation", + "operation_version", + "purpose", + "feed_identity", + "source_period", + "target_period", + "source_schedule", + "target_schedule", + "growth", + "subperiods_per_year", + "projection_policy", + "rounding", + "expected_operational_factor", + "validity", + "evidence", + "decision_references", + } +) +_VALIDATION_FIELDS = frozenset( + { + "unit", + "source_sha256", + "source_vintage", + "entity_role", + "provenance_class", + "assertion", + "period_coverage", + "source_column_id", + } +) + + +def factor_document_sha256(document: object) -> str: + """Hash canonical declaration/evidence bytes without a registry identity.""" + return hashlib.sha256(canonical_json(document)).hexdigest() + + +def _mapping(value: object, label: str) -> dict: + if not isinstance(value, Mapping): + raise ValueError(f"{label} must be an object.") + return dict(value) + + +def _closed(value: object, fields: set | frozenset, label: str) -> dict: + result = _mapping(value, label) + unknown = result.keys() - fields + if unknown: + raise ValueError(f"Unknown {label} fields: {sorted(unknown)}.") + return result + + +def _closed_feed_identity(value: object, label: str) -> dict: + identity = _mapping(value, label) + normalized = chronicle_feed_identity(identity) + unknown = identity.keys() - normalized.keys() + if unknown: + raise ValueError(f"Unknown {label} fields: {sorted(unknown)}.") + return normalized + + +def _list(value: object, label: str) -> list: + if not isinstance(value, (list, tuple)) or not value: + raise ValueError(f"{label} must be a nonempty list.") + return list(value) + + +def _decimal(value: object, label: str) -> Fraction: + if isinstance(value, bool) or not isinstance(value, (str, int, Decimal)): + raise ValueError(f"{label} requires a finite lexical decimal, never a float.") + try: + result = Decimal(value) + except (InvalidOperation, ValueError) as exc: + raise ValueError(f"{label} requires a finite decimal.") from exc + if not result.is_finite(): + raise ValueError(f"{label} requires a finite decimal.") + return Fraction(result) + + +def _rational(value: Fraction) -> dict: + return {"numerator": value.numerator, "denominator": value.denominator} + + +def _read_rational(value: object, label: str) -> Fraction: + row = _closed(value, {"numerator", "denominator"}, label) + numerator, denominator = row.get("numerator"), row.get("denominator") + if type(numerator) is not int or type(denominator) is not int or denominator <= 0: + raise ValueError( + f"{label} requires integer numerator and positive denominator." + ) + return Fraction(numerator, denominator) + + +def _integer_root(value: int, degree: int) -> int: + lower, upper = 0, 1 << ((value.bit_length() + degree - 1) // degree) + while lower < upper: + middle = (lower + upper + 1) // 2 + if middle**degree <= value: + lower = middle + else: + upper = middle - 1 + return lower + + +def certified_nth_root_bounds( + value: Fraction, degree: int, decimal_places: int +) -> tuple[Fraction, Fraction, bool]: + """Enclose a positive root on an integer grid; mark only exact roots exact.""" + if value <= 0 or type(degree) is not int or degree <= 0: + raise ValueError("Root radicand and degree must be positive.") + if type(decimal_places) is not int or not 0 <= decimal_places <= 4096: + raise ValueError("Root precision must be between 0 and 4096.") + numerator_root = _integer_root(value.numerator, degree) + denominator_root = _integer_root(value.denominator, degree) + if ( + numerator_root**degree == value.numerator + and denominator_root**degree == value.denominator + ): + exact = Fraction(numerator_root, denominator_root) + return exact, exact, True + scale = 10**decimal_places + floor = _integer_root(value.numerator * scale**degree // value.denominator, degree) + return Fraction(floor, scale), Fraction(floor + 1, scale), False + + +def _round_nearest(value: Fraction, digits: int) -> int: + scaled = value * 10**digits + quotient, remainder = divmod(scaled.numerator, scaled.denominator) + twice = 2 * remainder + return quotient + ( + twice > scaled.denominator + or (twice == scaled.denominator and quotient % 2 == 1) + ) + + +def _declaration(declaration: object) -> dict: + row = _closed(declaration, _DECLARATION_FIELDS, "factor declaration") + missing = _DECLARATION_FIELDS - row.keys() + if missing: + raise ValueError(f"Factor declaration missing fields: {sorted(missing)}.") + if type(row["schema_version"]) is not int or row["schema_version"] != 1: + raise ValueError("Factor declaration requires schema_version 1.") + if ( + row["operation"] != OPERATION + or type(row["operation_version"]) is not int + or row["operation_version"] != OPERATION_VERSION + ): + raise ValueError("Unknown factor operation or operation version.") + if not isinstance(row["id"], str) or not row["id"]: + raise ValueError("Factor id must be nonempty text.") + if row["purpose"] not in {"profile_input", "diagnostic"}: + raise ValueError("Unknown factor purpose.") + for field in ("source_period", "target_period"): + label = row[field] + if type(label) not in {str, int} or isinstance(label, str) and not label: + raise ValueError("Factor period labels require nonempty text or integers.") + if row["projection_policy"] != "observed_preferred_latest_contiguous": + raise ValueError("Unknown factor projection policy.") + frequency = row["subperiods_per_year"] + if type(frequency) is not int or not 1 <= frequency <= 366: + raise ValueError("subperiods_per_year must be a declared positive integer.") + rounding = _closed(row["rounding"], {"digits", "mode"}, "factor rounding") + if type(rounding.get("digits")) is not int or not 0 <= rounding["digits"] <= 100: + raise ValueError("Factor rounding digits must be an integer from 0 to 100.") + if rounding.get("mode") != "nearest_half_even": + raise ValueError("Factor rounding must be nearest_half_even.") + expected = _read_rational(row["expected_operational_factor"], "expected factor") + if ( + expected <= 0 + or row["expected_operational_factor"]["denominator"] != 10 ** rounding["digits"] + ): + raise ValueError("Expected operational factor must be positive p / 10**digits.") + validity = _closed( + row["validity"], + {"declared_on", "valid_from", "valid_until", "as_of"}, + "factor validity", + ) + try: + dates = { + key: date.fromisoformat(validity[key]) + for key in ("declared_on", "valid_from", "valid_until", "as_of") + } + except (KeyError, TypeError, ValueError) as exc: + raise ValueError("Factor validity requires explicit ISO dates.") from exc + if ( + not dates["declared_on"] <= dates["as_of"] + or not dates["valid_from"] <= dates["as_of"] <= dates["valid_until"] + ): + raise ValueError( + "Factor declaration is expired or not yet valid at its declared as_of." + ) + if not dates["valid_from"] <= date.today() <= dates["valid_until"]: + raise ValueError("Factor declaration is expired or not yet valid today.") + for key in ("evidence", "decision_references"): + if any(not isinstance(item, str) or not item for item in _list(row[key], key)): + raise ValueError(f"{key} must contain nonempty text.") + row["feed_identity"] = _closed_feed_identity( + row["feed_identity"], "factor declaration feed identity" + ) + return row + + +def _schedule(value: object, frequency: int, *, target: bool) -> list[dict]: + rows = _list(value, "factor schedule") + if len(rows) != frequency: + raise ValueError("Factor schedule must contain every declared subperiod once.") + result, previous_end, periods = [], None, set() + for index, raw in enumerate(rows): + fields = {"selector", "validation", "weight", "sequence"} + if target: + fields.add("projection_offset") + row = _closed(raw, fields, "factor schedule slot") + if type(row.get("sequence")) is not int or row["sequence"] != index: + raise ValueError("Factor schedule is misordered or noncontiguous.") + if target and ( + type(row.get("projection_offset")) is not int + or row["projection_offset"] != index + ): + raise ValueError("Invalid projection offsets.") + selector = _mapping(row.get("selector"), "factor selector") + if not selector or any( + not selector_field_is_supported(key) for key in selector + ): + raise ValueError( + "Unknown or empty factor selector; use the closed selector vocabulary." + ) + if "period_value" not in selector or isinstance( + selector["period_value"], (list, tuple) + ): + raise ValueError("Each schedule slot must select one explicit period.") + period = str(selector["period_value"]) + if period in periods: + raise ValueError("Duplicate factor schedule period.") + periods.add(period) + validation = _validation(row.get("validation")) + coverage = validation["period_coverage"] + try: + start, end = ( + date.fromisoformat(coverage["start_date"]), + date.fromisoformat(coverage["end_date"]), + ) + except (KeyError, TypeError, ValueError) as exc: + raise ValueError("Factor schedules require ISO period coverage.") from exc + if start > end or ( + previous_end is not None and start != previous_end + timedelta(days=1) + ): + raise ValueError("Factor schedule coverage is misordered or noncontiguous.") + previous_end = end + if _decimal(row.get("weight"), "schedule weight") <= 0: + raise ValueError("Averaging weights must be positive.") + result.append(row) + return result + + +def _validation(value: object) -> dict: + result = _closed(value, _VALIDATION_FIELDS, "factor operand validation") + required = _VALIDATION_FIELDS - {"source_column_id"} + if required - result.keys(): + raise ValueError( + "Factor operands require unit, source hash/vintage, role, assertion, provenance and coverage." + ) + if result["assertion"] not in {"observation", "source_projection"}: + raise ValueError("Unknown factor assertion.") + coverage = _closed( + result["period_coverage"], + {"start_date", "end_date", "basis", "source_period_label", "notes"}, + "factor operand coverage", + ) + if not {"start_date", "end_date"} <= coverage.keys(): + raise ValueError("Factor operand coverage requires both endpoints.") + for key in required - {"period_coverage"}: + if not isinstance(result[key], str) or not result[key]: + raise ValueError("Factor operand validations require nonempty text.") + source_hash = result["source_sha256"] + if len(source_hash) != 64 or any( + character not in "0123456789abcdef" for character in source_hash + ): + raise ValueError("Factor operand source hash must be a lowercase SHA-256.") + return result + + +def _operand( + slot: dict, + facts: tuple, + *, + optional: bool = False, + assertion_policy: str = "observed_only", +) -> tuple[Fraction, dict] | None: + selector = slot["selector"] + # Exact keys do not make duplicate same-period source vintages unambiguous. + broad = { + key: value + for key, value in selector.items() + if key + not in { + "aggregate_fact_key", + "semantic_fact_key", + "legacy_fact_key", + "source_record_id", + "record_set_id", + "layout_record_set_id", + "record_set_spec_id", + "layout_record_set_spec_id", + } + } + if ( + "period_value" in broad + and sum(fact_matches_selector(fact, broad) for fact in facts) > 1 + ): + raise ValueError("Duplicate factor period across source vintages or facts.") + matches = [fact for fact in facts if fact_matches_selector(fact, selector)] + if not matches and optional: + return None + if len(matches) != 1: + raise ValueError( + f"Factor selector must resolve exactly once; found {len(matches)}." + ) + fact = matches[0] + validation = _validation(slot["validation"]) + actual = { + "unit": fact.get("observed_measure", {}).get("unit"), + "source_sha256": fact.get("source", {}).get("source_sha256"), + "source_vintage": fact.get("source", {}).get("vintage"), + "entity_role": fact.get("entity", {}).get("role"), + "provenance_class": fact.get("provenance_class"), + "assertion": fact.get("assertion", "observation"), + "period_coverage": fact.get("period_coverage", {}), + "source_column_id": fact.get("layout", {}).get("source_column_id"), + } + for key, expected in validation.items(): + if key == "period_coverage": + if any(actual[key].get(field) != item for field, item in expected.items()): + raise ValueError("Factor operand period coverage mismatch.") + elif actual[key] != expected: + raise ValueError(f"Factor operand {key} mismatch.") + # Reuse the existing target assertion-policy vocabulary after all match paths. + if ( + actual["assertion"] == "source_projection" + and assertion_policy != "allow_source_projection" + ): + raise ValueError("Source projections require allow_source_projection.") + value = fact.get("value") + if isinstance(value, bool): + raise ValueError("Boolean factor operand is not numeric.") + if isinstance(value, LexicalFloat): + token = value.lexical_token + recovered = str(float(value)) + if _decimal(token, "feed numeric token") != _decimal( + recovered, "float decimal recovery" + ): + raise ValueError( + "Float conversion does not reproduce the feed lexical numeric token." + ) + proof = { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": token, + "recovered_decimal": recovered, + "decimal_values_equal": True, + } + elif type(value) is int: + token = str(value) + proof = { + "numeric_representation": "exact_integer", + "lexical_token": token, + "recovered_decimal": token, + "decimal_values_equal": True, + } + elif isinstance(value, Decimal): + token = str(value) + proof = { + "numeric_representation": "exact_decimal", + "lexical_token": token, + "recovered_decimal": token, + "decimal_values_equal": True, + } + else: + raise ValueError("Factor operand lacks an authenticated lexical numeric token.") + numeric = _decimal(token, "feed numeric token") + return numeric, { + "selector": selector, + "aggregate_fact_key": fact.get("aggregate_fact_key"), + "decimal_operand": token, + "numeric_token_proof": proof, + "validation": validation, + "period": fact.get("period"), + "source": fact.get("source"), + "unit": actual["unit"], + "assertion": actual["assertion"], + "provenance_class": actual["provenance_class"], + "entity_role": actual["entity_role"], + "period_coverage": actual["period_coverage"], + } + + +def resolve_reference_factor( + declaration: Mapping, facts: Iterable, feed_identity: Mapping +) -> tuple[dict, dict]: + """Resolve one closed operation, returning its operational rational and proof.""" + row = _declaration(declaration) + feed_identity = chronicle_feed_identity( + _mapping(feed_identity, "factor feed identity") + ) + if canonical_json(row["feed_identity"]) != canonical_json(feed_identity): + raise ValueError("Factor feed identity mismatch; re-declaration is required.") + fact_rows = tuple(facts) + frequency = row["subperiods_per_year"] + source = _schedule(row["source_schedule"], frequency, target=False) + target = _schedule(row["target_schedule"], frequency, target=True) + source_values, source_evidence = [], [] + for slot in source: + if slot["validation"]["assertion"] != "observation": + raise ValueError("Source schedule requires observations.") + value, evidence = _operand(slot, fact_rows) + if value <= 0: + raise ValueError("Factor denominator operands must be positive.") + source_values.append((value, _decimal(slot["weight"], "source weight"))) + evidence.update({"weight": slot["weight"], "sequence": slot["sequence"]}) + source_evidence.append(evidence) + denominator = sum(value * weight for value, weight in source_values) / sum( + weight for _, weight in source_values + ) + growth = row["growth"] + growth_evidence, effective_growth = None, Fraction(0) + if growth is not None: + growth = _closed( + growth, + { + "selector", + "validation", + "unit_multiplier", + "scenario_delta", + "sensitivity_provenance", + "assertion_policy", + }, + "factor growth", + ) + _validation(growth.get("validation")) + selector = _mapping(growth.get("selector"), "growth selector") + if not selector or any( + not selector_field_is_supported(key) for key in selector + ): + raise ValueError("Unknown growth selector field.") + policy = growth.get("assertion_policy") + if policy not in ALLOWED_ASSERTION_POLICIES: + raise ValueError("Unknown growth assertion policy.") + growth_value, growth_evidence = _operand( + growth, fact_rows, assertion_policy=policy + ) + multiplier = _decimal(growth.get("unit_multiplier"), "growth unit multiplier") + delta = _decimal(growth.get("scenario_delta"), "scenario delta") + if multiplier <= 0: + raise ValueError("Growth unit multiplier must be positive.") + if any( + not isinstance(item, str) or not item + for item in _list( + growth.get("sensitivity_provenance"), "sensitivity provenance" + ) + ): + raise ValueError("Missing sensitivity provenance.") + effective_growth = (growth_value + delta) * multiplier + growth_evidence.update( + { + "unit_multiplier": growth["unit_multiplier"], + "scenario_delta": growth["scenario_delta"], + "sensitivity_provenance": growth["sensitivity_provenance"], + "effective_growth": _rational(effective_growth), + } + ) + if 1 + effective_growth <= 0: + raise ValueError("Effective annual growth must have positive 1 + g.") + observations, target_evidence, gap = [], [], False + for slot in target: + if slot["validation"]["assertion"] != "observation": + raise ValueError("Target schedules prefer observations only.") + resolved = _operand(slot, fact_rows, optional=slot["sequence"] != 0) + if resolved is None: + gap = True + observations.append(None) + target_evidence.append( + { + "selector": slot["selector"], + "validation": slot["validation"], + "status": "projected", + } + ) + else: + if gap: + raise ValueError( + "Observed target subperiods must be contiguous from the anchor." + ) + value, evidence = resolved + if value <= 0: + raise ValueError("Observed target anchors must be positive.") + observations.append(value) + target_evidence.append({**evidence, "status": "observed"}) + if gap and growth is None: + raise ValueError("Projected target periods require a declared growth operand.") + last_observed = max( + index for index, value in enumerate(observations) if value is not None + ) + anchor = observations[last_observed] + target_weights = [_decimal(slot["weight"], "target weight") for slot in target] + + def numerator_at(root: Fraction) -> Fraction: + return sum( + (value if value is not None else anchor * root ** (index - last_observed)) + * target_weights[index] + for index, value in enumerate(observations) + ) / sum(target_weights) + + digits = row["rounding"]["digits"] + precision = digits + 8 + while precision <= 4096: + lower, upper, exact = certified_nth_root_bounds( + 1 + effective_growth, frequency, precision + ) + numerator_lower, numerator_upper = numerator_at(lower), numerator_at(upper) + factor_lower, factor_upper = ( + numerator_lower / denominator, + numerator_upper / denominator, + ) + rounded_lower, rounded_upper = ( + _round_nearest(factor_lower, digits), + _round_nearest(factor_upper, digits), + ) + if rounded_lower == rounded_upper: + break + precision *= 2 + else: + raise ValueError("Factor rounding cannot be certified unambiguously.") + factor = {"numerator": rounded_lower, "denominator": 10**digits} + if rounded_lower <= 0: + raise ValueError("Operational factor must be positive.") + if factor != row["expected_operational_factor"]: + raise ValueError( + f"Declared expected factor mismatch: computed {factor}; re-declaration is required." + ) + for index, evidence in enumerate(target_evidence): + evidence.update( + { + "sequence": index, + "weight": target[index]["weight"], + "projection_offset": 0 + if observations[index] is not None + else index - last_observed, + } + ) + if observations[index] is None: + evidence["projection_anchor"] = { + "aggregate_fact_key": target_evidence[last_observed][ + "aggregate_fact_key" + ] + } + receipt = { + "schema_version": 1, + "operation": OPERATION, + "operation_version": OPERATION_VERSION, + "factor_id": row["id"], + "declaration_sha256": factor_document_sha256(row), + "feed_identity": dict(feed_identity), + "source_period": row["source_period"], + "target_period": row["target_period"], + "source_schedule": source_evidence, + "target_schedule": target_evidence, + "subperiods_per_year": frequency, + "projection_policy": row["projection_policy"], + "denominator_mean": _rational(denominator), + "growth": growth_evidence, + "root_bounds": { + "lower": _rational(lower), + "upper": _rational(upper), + "exact": exact, + "decimal_places": precision, + "degree": frequency, + "radicand": _rational(1 + effective_growth), + }, + "numerator_mean_bounds": { + "lower": _rational(numerator_lower), + "upper": _rational(numerator_upper), + }, + "unrounded_factor_bounds": { + "lower": _rational(factor_lower), + "upper": _rational(factor_upper), + }, + "rounding": row["rounding"], + "operational_factor": factor, + "validity": row["validity"], + "evidence": row["evidence"], + "decision_references": row["decision_references"], + } + receipt["receipt_sha256"] = factor_document_sha256(receipt) + return factor, receipt + + +def validate_factor_context( + context: Mapping, selected_ids: Iterable[str] | None = None +) -> dict[str, dict]: + """Check selected declared evidence without claiming source authentication. + + Only selected declarations/evidence are validated or returned. Independent + profile edits therefore cannot change a branch's identity or availability. + """ + document = _closed( + context, + {"schema_version", "factors", "country", "feed_identity"}, + "factor context", + ) + if ( + type(document.get("schema_version")) is not int + or document["schema_version"] != 1 + ): + raise ValueError("Factor context requires schema_version 1.") + if "feed_identity" in document: + document["feed_identity"] = _closed_feed_identity( + document["feed_identity"], "factor context feed identity" + ) + entries = _list(document.get("factors"), "factor context entries") + by_id = {} + for raw in entries: + entry = _mapping(raw, "factor context entry") + declaration = _mapping(entry.get("declaration"), "factor declaration") + factor_id = declaration.get("id") + if not isinstance(factor_id, str) or not factor_id or factor_id in by_id: + raise ValueError("Factor ids must be unique nonempty text.") + by_id[factor_id] = entry + selected = list(by_id) if selected_ids is None else list(selected_ids) + result = {} + for factor_id in selected: + if factor_id not in by_id: + raise ValueError(f"Missing selected factor {factor_id!r}.") + entry = _closed( + by_id[factor_id], + {"declaration", "operational_factor", "receipt"}, + "factor context entry", + ) + declaration = _declaration(entry["declaration"]) + if ( + "feed_identity" in document + and document["feed_identity"] != declaration["feed_identity"] + ): + raise ValueError( + "Factor context feed identity is incompatible with the declaration." + ) + factor = entry.get("operational_factor") + _read_rational(factor, "declared operational factor") + if factor != declaration["expected_operational_factor"]: + raise ValueError( + "Declared operational factor differs from its expectation." + ) + receipt = _mapping(entry.get("receipt"), "factor receipt") + body = {key: value for key, value in receipt.items() if key != "receipt_sha256"} + if receipt.get("receipt_sha256") != factor_document_sha256(body): + raise ValueError("Factor receipt hash mismatch.") + if ( + type(receipt.get("schema_version")) is not int + or receipt.get("schema_version") != 1 + or receipt.get("operation") != declaration["operation"] + or type(receipt.get("operation_version")) is not int + or receipt.get("operation_version") != declaration["operation_version"] + or receipt.get("factor_id") != factor_id + or receipt.get("declaration_sha256") != factor_document_sha256(declaration) + or receipt.get("operational_factor") != factor + or receipt.get("feed_identity") != declaration["feed_identity"] + ): + raise ValueError("Factor declaration and receipt are incompatible.") + result[factor_id] = entry + return result + + +def replay_reference_factor( + declaration: Mapping, + facts: Iterable, + feed_identity: Mapping, + operational_factor: Mapping, + receipt: Mapping, +) -> None: + """Authenticate evidence by recomputing it from the loaded feed facts.""" + factor, recomputed = resolve_reference_factor(declaration, facts, feed_identity) + if factor != operational_factor or canonical_json(recomputed) != canonical_json( + receipt + ): + raise ValueError("Factor replay differs from declared result or evidence.") diff --git a/packages/microcosm-build/src/microcosm/build/reference_profiles.py b/packages/microcosm-build/src/microcosm/build/reference_profiles.py index 9a11ac22af..881fdbf496 100644 --- a/packages/microcosm-build/src/microcosm/build/reference_profiles.py +++ b/packages/microcosm-build/src/microcosm/build/reference_profiles.py @@ -19,7 +19,13 @@ from microcosm.graph.canonical import canonical_json REFERENCE_PROFILE_RESOLVER_VERSION = "reference-profiles-v1" +FACTOR_REFERENCE_PROFILE_RESOLVER_VERSION = "reference-profiles-v2" +ALLOWED_REFERENCE_PROFILE_RESOLVER_VERSIONS = frozenset( + {REFERENCE_PROFILE_RESOLVER_VERSION, FACTOR_REFERENCE_PROFILE_RESOLVER_VERSION} +) +SCALE_MONEY_AND_INTERVALS = "scale_money_and_intervals" REQUIRES_REFERENCE_PROFILE_RESOLUTION = "requires_reference_profile_resolution" +REQUIRES_PREREQUISITES = "requires_prerequisites" REFERENCE_DOCUMENT_KEYS = frozenset( {"country", "schema_version", "hierarchy", "target_references"} ) @@ -47,7 +53,7 @@ def _sequence(value: object, context: str) -> list: def _selection_identity(receipt: Mapping) -> dict: - return { + identity = { key: receipt[key] for key in ( "resolver_version", @@ -57,6 +63,161 @@ def _selection_identity(receipt: Mapping) -> dict: "selection", ) } + if receipt.get("resolver_version") == FACTOR_REFERENCE_PROFILE_RESOLVER_VERSION: + identity["selected_factor_identities"] = receipt["selected_factor_identities"] + return identity + + +def validate_reference_factor_evidence(receipt: Mapping) -> dict: + """Bind selected declarations and receipts without claiming feed authenticity.""" + from microcosm.build.reference_factors import validate_factor_context + + if receipt.get("resolver_version") == REFERENCE_PROFILE_RESOLVER_VERSION: + if "selected_factors" in receipt or "selected_factor_identities" in receipt: + raise ValueError("Legacy resolution cannot carry factor evidence.") + return {} + factors = _mapping(receipt.get("selected_factors"), "selected factors") + if not factors: + raise ValueError("Factor resolution needs selected factor evidence.") + validated = validate_factor_context( + {"schema_version": 1, "factors": list(factors.values())}, list(factors) + ) + if validated != factors: + raise ValueError("Selected factor identities do not match their declarations.") + identities = { + factor_id: { + "declaration_sha256": reference_document_sha256(entry["declaration"]), + "receipt_sha256": entry["receipt"]["receipt_sha256"], + } + for factor_id, entry in sorted(factors.items()) + } + if identities != receipt.get("selected_factor_identities"): + raise ValueError("Selected factor evidence identity mismatch.") + return validated + + +def _profile_metadata(transform: Mapping) -> dict: + metadata = _mapping(transform.get("metadata", {}), "profile metadata") + if any( + not isinstance(key, str) or not isinstance(value, str) or not value + for key, value in metadata.items() + ): + raise ValueError("Profile metadata requires nonempty strings.") + if set(metadata) & { + "activation_status", + "activation_reason", + "parking_reason", + "required_reference_profile", + "required_measurement_binding", + "reference_value_kind", + "reference_prerequisites", + "reference_prerequisites_remaining", + "reference_selection_sha256", + "effective_intervals_sha256", + "reference_profiles_resolved", + }: + raise ValueError("A profile cannot override resolution identities.") + return metadata + + +def _explicit_names(transform: Mapping, field: str) -> list[str]: + names = _sequence(transform.get(field), field) + if ( + not names + or any(not isinstance(name, str) or not name for name in names) + or len(set(names)) != len(names) + ): + raise ValueError("A transformation needs unique explicit reference names.") + return names + + +def _companion_key(row: Mapping, *, scope: bool = False) -> bytes: + selector = _mapping(row.get("ledger_selector"), "companion selector") + excluded = {"aggregate_fact_key", "source_record_id"} + if not scope: + excluded.update({"source_measure_id", "source_concept"}) + else: + excluded.add("dimension_values") + payload = { + "selector": { + key: value for key, value in selector.items() if key not in excluded + }, + "entity": row.get("entity"), + "period": row.get("period"), + "reference_set": row.get("metadata", {}).get("reference_set"), + } + if not scope: + payload["filter"] = row.get("filter") + return canonical_json(payload) + + +def _validate_companions(transform: Mapping, by_name: Mapping) -> tuple[list, list]: + money = _explicit_names(transform, "reference_names") + counts = _explicit_names(transform, "count_reference_names") + if set(money) & set(counts) or len(money) != len(counts): + raise ValueError("Money and count rosters need disjoint complete companions.") + for kind, names in (("money", money), ("count", counts)): + if set(names) - set(by_name): + raise ValueError("A profile names missing companion references.") + if any( + by_name[name].get("metadata", {}).get("reference_value_kind") != kind + for name in names + ): + raise ValueError( + "Money and count rosters require matching declared value kinds." + ) + scopes = {_companion_key(by_name[name], scope=True) for name in names} + complete = { + name + for name, row in by_name.items() + if isinstance(row.get("ledger_selector"), Mapping) + and _companion_key(row, scope=True) in scopes + } + if complete != set(names): + raise ValueError("Money and count rosters are incomplete companions.") + money_keys = [_companion_key(by_name[name]) for name in money] + count_keys = [_companion_key(by_name[name]) for name in counts] + if len(set(money_keys)) != len(money) or set(money_keys) != set(count_keys): + raise ValueError("Money and count rosters must match one-to-one companions.") + if {_companion_key(by_name[name], scope=True) for name in money} & { + _companion_key(by_name[name], scope=True) for name in counts + }: + raise ValueError("Money and count companions must select distinct measures.") + return money, counts + + +def _satisfy_factor_prerequisite(row: dict, dimension: str, binding: str) -> list: + metadata = row.setdefault("metadata", {}) + if metadata.get("activation_status") != REQUIRES_PREREQUISITES: + raise ValueError( + "Money scaling requires the explicit prerequisite parking status." + ) + if ( + metadata.get("required_reference_profile") != dimension + or metadata.get("required_measurement_binding") != binding + ): + raise ValueError("Money scaling has incompatible reference prerequisites.") + try: + prerequisites = json.loads(metadata.get("reference_prerequisites", "")) + except (TypeError, ValueError) as error: + raise ValueError( + "Reference prerequisites must be an explicit JSON list." + ) from error + required = {f"reference_profile:{dimension}", f"measurement:{binding}"} + if ( + not isinstance(prerequisites, list) + or any(not isinstance(item, str) or not item for item in prerequisites) + or len(set(prerequisites)) != len(prerequisites) + or set(prerequisites) != required + ): + raise ValueError( + "Reference prerequisites must name the profile and measurement." + ) + remaining = [ + item for item in prerequisites if item != f"reference_profile:{dimension}" + ] + metadata["reference_prerequisites_remaining"] = canonical_json(remaining).decode() + return remaining def resolve_reference_profiles( @@ -64,13 +225,16 @@ def resolve_reference_profiles( profile_declarations: Mapping, selection: Mapping, interval_document: Mapping | None = None, + *, + factor_context: Mapping | None = None, ) -> tuple[dict, dict | None, dict]: """Return effective references, effective intervals and a bound receipt. Only selected declarations enter selection identity. An unavailable profile refuses; an explicitly deferred dimension is reported without claiming its - transformations ran. Interval transformations are intentionally unsupported - until a consumer declares their complete transformation contract. + transformations ran. Money scaling consumes declared factor evidence; + compilation authenticates it against the feed. Interval scaling is reserved + for the subsequent interval implementation. Resource headers are bound into the base identity; effective references use the compiler's closed document shape. """ @@ -120,6 +284,22 @@ def resolve_reference_profiles( deferred.append(dimension) chosen[dimension] = {"name": name, "declaration": profile} resolved_selection[dimension] = name + factor_ids = set() + for choice in chosen.values(): + for raw_transform in choice["declaration"].get("transformations", []): + transform = _mapping(raw_transform, "reference transformation") + if transform.get("operation") == SCALE_MONEY_AND_INTERVALS: + if _sequence(transform.get("interval_table_ids"), "interval table ids"): + raise ValueError("interval scaling lands in r2b") + factor_id = transform.get("factor_id") + if not isinstance(factor_id, str) or not factor_id: + raise ValueError("Money scaling needs a declared factor id.") + factor_ids.add(factor_id) + selected_factors = {} + if factor_ids: + from microcosm.build.reference_factors import validate_factor_context + + selected_factors = validate_factor_context(factor_context, sorted(factor_ids)) receipt = { "resolver_version": REFERENCE_PROFILE_RESOLVER_VERSION, "base_references_sha256": reference_document_sha256(base), @@ -129,12 +309,27 @@ def resolve_reference_profiles( "selected_declarations_sha256": reference_document_sha256(chosen), "selection": resolved_selection, } + if selected_factors: + receipt.update( + resolver_version=FACTOR_REFERENCE_PROFILE_RESOLVER_VERSION, + selected_factors=_copy(selected_factors), + selected_factor_identities={ + factor_id: { + "declaration_sha256": reference_document_sha256( + entry["declaration"] + ), + "receipt_sha256": entry["receipt"]["receipt_sha256"], + } + for factor_id, entry in sorted(selected_factors.items()) + }, + ) selection_hash = reference_document_sha256(_selection_identity(receipt)) interval_hash = None if intervals is None else reference_document_sha256(intervals) transformed, policies = {}, [] for dimension, choice in chosen.items(): for raw_transform in choice["declaration"].get("transformations", []): transform = _mapping(raw_transform, "reference transformation") + operation = transform.get("operation") allowed = { "reference_names", "operation", @@ -143,30 +338,113 @@ def resolve_reference_profiles( "notes", "target_label", } + if operation == SCALE_MONEY_AND_INTERVALS: + allowed = { + "reference_names", + "count_reference_names", + "operation", + "factor_id", + "interval_table_ids", + "required_measurement_binding", + "metadata", + } if set(transform) - allowed: raise ValueError("Unknown reference transformation fields.") - if transform.get("operation") != ALLOCATED_RESIDUAL_BY_RATIO: + if operation not in { + ALLOCATED_RESIDUAL_BY_RATIO, + SCALE_MONEY_AND_INTERVALS, + }: raise ValueError("Unsupported reference profile transformation.") + metadata = _profile_metadata(transform) + if _carried_value_keys(transform): + raise ValueError("Reference transformations carry observed values.") + if operation == SCALE_MONEY_AND_INTERVALS: + tables = _sequence( + transform.get("interval_table_ids"), "interval table ids" + ) + if tables: + raise ValueError("interval scaling lands in r2b") + binding = transform.get("required_measurement_binding") + if not isinstance(binding, str) or not binding: + raise ValueError( + "Money scaling needs a required measurement binding." + ) + money, counts = _validate_companions(transform, by_name) + factor_id = transform["factor_id"] + factor = selected_factors[factor_id] + declaration = factor["declaration"] + if declaration["purpose"] != "profile_input": + raise ValueError("A diagnostic factor cannot be a profile input.") + rational = factor["operational_factor"] + weight = rational["numerator"] / rational["denominator"] + for name in [*money, *counts]: + if name in transformed: + raise ValueError( + f"Duplicate or incompatible transformation: {name!r}." + ) + row = by_name[name] + if ( + row.get("ledger_selector", {}).get("period_value") + != declaration["source_period"] + or row.get("period") != declaration["target_period"] + ): + raise ValueError( + "Factor and reference source/target periods are incompatible." + ) + remaining = _satisfy_factor_prerequisite(row, dimension, binding) + if name in money: + if row.get( + "value_operation", "identity" + ) != "identity" or row.get("value_operands", []): + raise ValueError( + "Money scaling requires an untransformed source reference." + ) + selector = _mapping( + row.get("ledger_selector"), "money selector" + ) + key = selector.get("aggregate_fact_key") + if not isinstance(key, str) or not key: + raise ValueError( + "Money scaling needs a singleton source fact key." + ) + row["value_operation"] = "linear_combination" + row["value_operands"] = [ + { + "aggregate_fact_key": key, + "expected_member_count": 1, + "weight": weight, + } + ] + row_metadata = row.setdefault("metadata", {}) + row_metadata.update(metadata) + row_metadata["reference_selection_sha256"] = selection_hash + row_metadata["reference_profiles_resolved"] = "1" + if interval_hash is not None: + row_metadata["effective_intervals_sha256"] = interval_hash + transformed[name] = { + "dimension": dimension, + "profile": choice["name"], + "operation": "linear_combination" + if name in money + else "identity", + "factor_id": factor_id, + "operational_factor": _copy(rational), + "metadata": metadata, + "operand_selectors": _copy(row.get("value_operands", [])), + "source_period": row.get("ledger_selector", {}).get( + "period_value" + ), + "target_period": row.get("period"), + "unresolved_prerequisites": remaining, + } + policies.append( + {"dimension": dimension, "profile": choice["name"], **metadata} + ) + continue names = _sequence(transform.get("reference_names"), "transformed names") if not names or any(not isinstance(name, str) for name in names): raise ValueError("A transformation needs explicit reference names.") operands = _sequence(transform.get("value_operands"), "value operands") - metadata = _mapping(transform.get("metadata", {}), "profile metadata") - if any( - not isinstance(key, str) or not isinstance(value, str) or not value - for key, value in metadata.items() - ): - raise ValueError("Profile metadata requires nonempty strings.") - if set(metadata) & { - "activation_status", - "required_reference_profile", - "reference_selection_sha256", - "effective_intervals_sha256", - "reference_profiles_resolved", - }: - raise ValueError("A profile cannot override resolution identities.") - if _carried_value_keys(transform): - raise ValueError("Reference transformations carry observed values.") for name in names: if name not in by_name: raise ValueError(f"Profile names missing reference {name!r}.") @@ -242,12 +520,16 @@ def validate_reference_resolution( ) -> dict: """Refuse stale effective documents, receipts or interval companions.""" resolution = _copy(_mapping(receipt, "reference resolution receipt")) - if resolution.get("resolver_version") != REFERENCE_PROFILE_RESOLVER_VERSION: + if ( + resolution.get("resolver_version") + not in ALLOWED_REFERENCE_PROFILE_RESOLVER_VERSIONS + ): raise ValueError("Unknown reference resolver version.") claimed_receipt_hash = resolution.pop("receipt_sha256", None) if claimed_receipt_hash != reference_document_sha256(resolution): raise ValueError("Reference resolution receipt hash mismatch.") resolution["receipt_sha256"] = claimed_receipt_hash + factors = validate_reference_factor_evidence(resolution) actual = reference_document_sha256(document) if actual != resolution.get("effective_references_sha256") or ( references_sha256 is not None and actual != references_sha256 @@ -263,7 +545,7 @@ def validate_reference_resolution( interval_document ) != resolution.get("effective_intervals_sha256"): raise ValueError("Effective interval document hash mismatch.") - names = [] + names, used_factors = [], set() for row in document.get("target_references", ()): metadata = row.get("metadata", {}) if metadata.get("reference_profiles_resolved"): @@ -273,6 +555,44 @@ def validate_reference_resolution( != resolution.get("effective_intervals_sha256") ): raise ValueError("Effective reference profile identity mismatch.") + action = resolution.get("transformations", {}).get(row["name"], {}) + if "factor_id" in action: + factor_id = action["factor_id"] + if factor_id not in factors: + raise ValueError( + "A transformed reference has missing factor evidence." + ) + used_factors.add(factor_id) + entry = factors[factor_id] + declaration = entry["declaration"] + rational = entry["operational_factor"] + if ( + action.get("operational_factor") != rational + or action.get("source_period") != declaration["source_period"] + or action.get("target_period") != declaration["target_period"] + ): + raise ValueError("Effective reference factor identity mismatch.") + if action.get("operation") == "linear_combination": + expected = [ + { + "aggregate_fact_key": row.get("ledger_selector", {}).get( + "aggregate_fact_key" + ), + "expected_member_count": 1, + "weight": rational["numerator"] / rational["denominator"], + } + ] + if ( + row.get("value_operation") != "linear_combination" + or row.get("value_operands") != expected + ): + raise ValueError( + "Effective money factor must be applied exactly once." + ) + elif action.get("operation") != "identity": + raise ValueError("Unsupported factor transformation action.") if sorted(names) != resolution.get("transformed_names"): raise ValueError("Resolution receipt transformed names mismatch.") + if used_factors != set(factors): + raise ValueError("Resolution receipt has unused selected factor evidence.") return resolution diff --git a/packages/microcosm-build/src/microcosm/build/transport/compose.py b/packages/microcosm-build/src/microcosm/build/transport/compose.py index 211cf380a9..82740818d2 100644 --- a/packages/microcosm-build/src/microcosm/build/transport/compose.py +++ b/packages/microcosm-build/src/microcosm/build/transport/compose.py @@ -68,7 +68,10 @@ ATOMIC_GEOGRAPHY_VALIDATION_TYPE, ATOMIC_SUPPORT_TYPE, ) -from microcosm.build.ledger_artifact import CHRONICLE_FEED_RESOURCE +from microcosm.build.ledger_artifact import ( + CHRONICLE_FEED_RESOURCE, + chronicle_feed_identity, +) from microcosm.build.reference_profiles import resolve_reference_profiles from microcosm.calibrate.ordered_kernels import OUTPUT_TYPES from microcosm.graph import ( @@ -386,14 +389,28 @@ def reference_profile_view( else {name: value for name, value in selection.items() if name in dimensions} ) intervals = declarations.get("interval_resource") - return resolve_reference_profiles( + factor_context_resource = declarations.get("factor_context_resource") + view = resolve_reference_profiles( _raw_transport_resource(spec, resource), declarations, selected, interval_document=None if intervals is None else _raw_transport_resource(spec, intervals), + factor_context=None + if factor_context_resource is None + else _raw_transport_resource(spec, factor_context_resource), ) + if _has_resource(spec, CHRONICLE_FEED_RESOURCE): + feed_pin = _raw_transport_resource(spec, CHRONICLE_FEED_RESOURCE) + for entry in view[2].get("selected_factors", {}).values(): + if entry["declaration"]["feed_identity"] != chronicle_feed_identity( + feed_pin + ): + raise ValueError( + "Selected factor context is incompatible with the declared feed." + ) + return view def transport_resource( @@ -536,6 +553,16 @@ def validate_transport_activation(spec: Mapping[str, object]) -> None: missing = [] # Resolve first so activation never inspects an executable profile's raw rows. reference_profile_view(spec) + if _has_resource(spec, "reference_profiles") and _has_resource(spec, "scenarios"): + checked_selections = {canonical_json({})} + for scenario in _sequence( + _raw_transport_resource(spec, "scenarios").get("scenarios"), "scenarios" + ): + knobs = _mapping(_mapping(scenario, "scenario").get("knobs", {}), "knobs") + identity = canonical_json(knobs) + if identity not in checked_selections: + reference_profile_view(spec, knobs) + checked_selections.add(identity) selections = ( tuple(_declared_resource_selections(transport_resource(spec, GRAPH_RESOURCE))) if _has_resource(spec, GRAPH_RESOURCE) diff --git a/packages/microcosm-build/src/microcosm/build/transport/target_kernels.py b/packages/microcosm-build/src/microcosm/build/transport/target_kernels.py index 2752a924a0..8e3c14221a 100644 --- a/packages/microcosm-build/src/microcosm/build/transport/target_kernels.py +++ b/packages/microcosm-build/src/microcosm/build/transport/target_kernels.py @@ -53,6 +53,7 @@ import microcosm.build.country_spec as country_spec_module import microcosm.build.ledger_artifact as ledger_artifact_module import microcosm.build.ledger_targets as ledger_targets_module +import microcosm.build.reference_factors as reference_factors_module import microcosm.build.reference_profiles as reference_profiles_module import microcosm.calibrate.artifacts as calibrate_artifacts_module import microcosm.calibrate.hierarchy as hierarchy_module @@ -71,7 +72,9 @@ compile_ledger_target_references, ) from microcosm.build.reference_profiles import ( + ALLOWED_REFERENCE_PROFILE_RESOLVER_VERSIONS, REFERENCE_DOCUMENT_KEYS, + validate_reference_factor_evidence, validate_reference_resolution, ) from microcosm.calibrate import TargetRegistry, TargetSpec @@ -274,8 +277,7 @@ def _surface_resolution(document: Mapping, registry: TargetRegistry) -> Mapping body = dict(resolution) claimed = body.pop("receipt_sha256", None) if ( - body.get("resolver_version") - != reference_profiles_module.REFERENCE_PROFILE_RESOLVER_VERSION + body.get("resolver_version") not in ALLOWED_REFERENCE_PROFILE_RESOLVER_VERSIONS or claimed != reference_profiles_module.reference_document_sha256(body) ): raise ValueError("Surface reference resolution receipt hash mismatch.") @@ -291,6 +293,7 @@ def _surface_resolution(document: Mapping, registry: TargetRegistry) -> Mapping ) from error if selection != resolution.get("reference_selection_sha256"): raise ValueError("Surface reference selection identity mismatch.") + validate_reference_factor_evidence(resolution) for spec in resolved: if spec.metadata.get( "reference_selection_sha256" @@ -398,6 +401,30 @@ def compile_target_surface( artifact = load_ledger_consumer_artifact( facts_path, expected_facts_sha256=expected_facts_sha256, feed_pin=feed_pin ) + if reference_resolution is not None: + for entry in reference_resolution.get("selected_factors", {}).values(): + factor_feed_identity = ledger_artifact_module.chronicle_feed_identity( + feed_pin + if feed_pin is not None + else entry["declaration"]["feed_identity"] + ) + if feed_pin is None: + ledger_artifact_module.validate_chronicle_feed_pin( + artifact, factor_feed_identity + ) + if (artifact.manifest or {}).get( + "source_commit" + ) != factor_feed_identity["source_commit"]: + raise ValueError( + "Factor source commit cannot be authenticated without a feed pin." + ) + reference_factors_module.replay_reference_factor( + entry["declaration"], + artifact.facts, + factor_feed_identity, + entry["operational_factor"], + entry["receipt"], + ) registry = compile_ledger_target_references( artifact.facts, references, country=country ) @@ -489,6 +516,7 @@ def _optional_float(value: float) -> float | None: ledger_targets_module, ledger_artifact_module, chronicle_epoch_module, + reference_factors_module, reference_profiles_module, registry_module, target_module, diff --git a/packages/microcosm-build/tests/engine_free/shared/test_calibration_columns.py b/packages/microcosm-build/tests/engine_free/shared/test_calibration_columns.py index c3eda2039d..283c146181 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_calibration_columns.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_calibration_columns.py @@ -182,10 +182,14 @@ def test_receipt_age_indicators_retain_the_published_strata_boundaries(): def test_all_active_calibration_bindings_enter_the_small_frame_matrix(): frame = _frame(prepare_calibration_columns(_tables(), _bindings())) root = paths_for("microcosm-build").package / "src/microcosm/build/nz" + profiles = json.loads((root / "reference_profiles.json").read_text()) effective, _, _ = resolve_reference_profiles( json.loads((root / "target_references.json").read_text()), - json.loads((root / "reference_profiles.json").read_text()), + profiles, {}, + factor_context=json.loads( + (root / profiles["factor_context_resource"]).read_text() + ), ) references = [ ref diff --git a/packages/microcosm-build/tests/engine_free/shared/test_ledger_artifact.py b/packages/microcosm-build/tests/engine_free/shared/test_ledger_artifact.py index 1307e09d06..557e6216da 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_ledger_artifact.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_ledger_artifact.py @@ -5,8 +5,10 @@ import hashlib import json import os +import pickle import runpy from argparse import ArgumentParser, Namespace +from copy import deepcopy from dataclasses import replace from pathlib import Path @@ -18,6 +20,7 @@ CONSUMER_ARTIFACT_SCHEMA_VERSION, ChronicleFeedPinMismatchError, LedgerConsumerArtifact, + LexicalFloat, add_ledger_artifact_args, load_ledger_consumer_artifact, resolve_ledger_artifact, @@ -87,6 +90,65 @@ def test_loads_bare_consumer_facts_file(tmp_path): assert provenance["schema_version"] is None +def test_authenticated_float_retains_lexical_token_without_changing_json(tmp_path): + facts_path = tmp_path / "consumer_facts.jsonl" + original = '{"aggregate_fact_key":"decimal","value":1.2500e2}\n' + facts_path.write_text(original) + facts_sha = hashlib.sha256(original.encode()).hexdigest() + artifact = load_ledger_consumer_artifact( + facts_path, expected_facts_sha256=facts_sha + ) + value = artifact.facts[0]["value"] + assert isinstance(value, LexicalFloat) + assert not hasattr(value, "__dict__") + assert value.lexical_token == "1.2500e2" + assert value == 125.0 + assert hash(value) == hash(125.0) + assert json.dumps(value).encode() == json.dumps(125.0).encode() + assert json.dumps(artifact.facts[0], sort_keys=True) == json.dumps( + json.loads(original), sort_keys=True + ) + assert artifact.facts_sha256 == facts_sha + + +@pytest.mark.parametrize("protocol", range(pickle.HIGHEST_PROTOCOL + 1)) +def test_authenticated_float_pickle_preserves_lexical_token(tmp_path, protocol): + facts_path = tmp_path / "consumer_facts.jsonl" + facts_path.write_text('{"aggregate_fact_key":"decimal","value":1.2500e2}\n') + value = load_ledger_consumer_artifact(facts_path).facts[0]["value"] + + restored = pickle.loads(pickle.dumps(value, protocol=protocol)) + + assert isinstance(restored, LexicalFloat) + assert restored.lexical_token == "1.2500e2" + assert restored == value + assert not hasattr(restored, "__dict__") + + +def test_authenticated_float_deepcopy_preserves_lexical_token(tmp_path): + facts_path = tmp_path / "consumer_facts.jsonl" + facts_path.write_text('{"aggregate_fact_key":"decimal","value":1.2500e2}\n') + value = load_ledger_consumer_artifact(facts_path).facts[0]["value"] + + restored = deepcopy(value) + + assert isinstance(restored, LexicalFloat) + assert restored.lexical_token == "1.2500e2" + assert restored == value + assert not hasattr(restored, "__dict__") + + +@pytest.mark.parametrize("token", ["125", "9007199254740993", str(10**100 + 1)]) +def test_authenticated_integer_tokens_stay_exact_integers(tmp_path, token): + facts_path = tmp_path / "consumer_facts.jsonl" + facts_path.write_text('{"aggregate_fact_key":"integer","value":' + token + "}\n") + + value = load_ledger_consumer_artifact(facts_path).facts[0]["value"] + + assert type(value) is int + assert value == int(token) + + def test_loads_artifact_directory_and_records_provenance(tmp_path): rows = [_fact_row(), _fact_row(assertion="source_projection")] artifact_dir = _write_artifact_dir(tmp_path, rows) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_nz_reference_aging.py b/packages/microcosm-build/tests/engine_free/shared/test_nz_reference_aging.py new file mode 100644 index 0000000000..448085b354 --- /dev/null +++ b/packages/microcosm-build/tests/engine_free/shared/test_nz_reference_aging.py @@ -0,0 +1,256 @@ +"""NZ feed operands, reviewed factors, and the observed-quarter re-declaration.""" + +from __future__ import annotations + +import copy +import json +import os +from decimal import Decimal, localcontext +from fractions import Fraction +from pathlib import Path + +import pytest + +from microcosm.build.ledger_artifact import ( + LexicalFloat, + load_ledger_consumer_artifact, + validate_chronicle_feed_pin, +) +from microcosm.build.ledger_targets import fact_matches_selector +from microcosm.build.reference_factors import ( + replay_reference_factor, + resolve_reference_factor, + validate_factor_context, +) +from test_support.paths import paths_for +from tools.generate_nz_reference_aging import ( + _OPERANDS, + build_reference_aging, +) + +_PATHS = paths_for("microcosm-build") +_SPEC = _PATHS.package / "src/microcosm/build/nz" +_EXPECTED = { + "ird_ty2024_to_ty2027_low": 1115056, + "ird_ty2024_to_ty2027_central": 1117109, + "ird_ty2024_to_ty2027_high": 1119158, + "p1_ty2026_to_ty2027_low": 1026690, + "p1_ty2026_to_ty2027_central": 1028580, + "p1_ty2026_to_ty2027_high": 1030467, + "observed_ty2026_over_ty2024": 1086069, +} + + +def _document(): + return json.loads((_SPEC / "reference_aging.json").read_text()) + + +def _entry(factor_id="ird_ty2024_to_ty2027_central"): + return next( + entry + for entry in _document()["factors"] + if entry["declaration"]["id"] == factor_id + ) + + +def _fraction(rational): + return Fraction(rational["numerator"], rational["denominator"]) + + +@pytest.fixture(scope="module") +def artifact(): + root = os.environ.get("NZ_CHRONICLE_ARTIFACT") + if not root: + pytest.skip("NZ_CHRONICLE_ARTIFACT is not set") + loaded = load_ledger_consumer_artifact(Path(root)) + validate_chronicle_feed_pin(loaded, _document()["feed_identity"]) + return loaded + + +@pytest.fixture(scope="module") +def operand_facts(artifact): + keys = {f"ledger.aggregate_fact.v2:{suffix}" for _, _, suffix, _ in _OPERANDS} + keys.add("ledger.aggregate_fact.v2:aba7d3d54489b068116e6a25") + return tuple(fact for fact in artifact.facts if fact["aggregate_fact_key"] in keys) + + +def test_generated_aging_resource_reproduces_pinned_feed_and_all_receipts(artifact): + assert build_reference_aging(artifact) == _document() + assert set(validate_factor_context(_document())) == set(_EXPECTED) + identity = _document()["feed_identity"] + assert set(identity) == { + "source_commit", + "facts_sha256", + "manifest_sha256", + "artifact_schema_version", + "consumer_fact_schema_versions", + "consumer_fact_schema_sha256", + "fact_row_count", + } + for entry in _document()["factors"]: + assert entry["declaration"]["feed_identity"] == identity + assert entry["receipt"]["feed_identity"] == identity + + +@pytest.mark.parametrize("factor_id", list(_EXPECTED)) +def test_all_six_factors_and_observed_diagnostic_are_exact(factor_id, operand_facts): + entry = _entry(factor_id) + declaration = entry["declaration"] + expected = {"numerator": _EXPECTED[factor_id], "denominator": 1000000} + factor, receipt = resolve_reference_factor( + declaration, operand_facts, _document()["feed_identity"] + ) + assert factor == expected == declaration["expected_operational_factor"] + assert receipt == entry["receipt"] + assert receipt["operation"] == "projected_period_average_ratio" + assert receipt["operation_version"] == 1 + assert receipt["subperiods_per_year"] == 4 + assert _fraction(receipt["denominator_mean"]) == ( + Fraction("1670.0725") if factor_id.startswith("p1_") else Fraction("1537.7225") + ) + assert declaration["purpose"] == ( + "diagnostic" if factor_id.startswith("observed_") else "profile_input" + ) + replay_reference_factor( + declaration, operand_facts, _document()["feed_identity"], factor, receipt + ) + + +def test_each_reviewed_operand_resolves_once_with_exact_lexical_evidence(artifact): + for line, period, suffix, expected in _OPERANDS: + fact = artifact.facts[line - 1] + assert fact["aggregate_fact_key"] == f"ledger.aggregate_fact.v2:{suffix}" + assert fact["period"]["value"] == period + assert fact["value"].lexical_token == expected + checked = set() + for entry in _document()["factors"]: + declaration = entry["declaration"] + slots = [*declaration["source_schedule"], *declaration["target_schedule"]] + if declaration["growth"]: + slots.append(declaration["growth"]) + for slot in slots: + identity = json.dumps( + {"selector": slot["selector"], "validation": slot["validation"]}, + sort_keys=True, + ) + if identity in checked: + continue + checked.add(identity) + matches = [ + fact + for fact in artifact.facts + if fact_matches_selector(fact, slot["selector"]) + ] + if not matches: + assert slot in declaration["target_schedule"] + assert slot["projection_offset"] > 0 + continue + (fact,) = matches + validation = slot["validation"] + assert fact["observed_measure"]["unit"] == validation["unit"] + assert fact["source"]["source_sha256"] == validation["source_sha256"] + assert fact["source"]["vintage"] == validation["source_vintage"] + assert fact["entity"]["role"] == validation["entity_role"] + assert fact["assertion"] == validation["assertion"] + assert fact["provenance_class"] == validation["provenance_class"] + for key in ("start_date", "end_date"): + assert ( + fact["period_coverage"][key] == validation["period_coverage"][key] + ) + + +def test_treasury_fy2027_source_column_uses_chronicle_2026_and_2027_refuses( + operand_facts, +): + declaration = _entry()["declaration"] + assert declaration["growth"]["selector"]["period_value"] == 2026 + assert declaration["growth"]["validation"]["source_column_id"] == ( + "year_ending_30_june_2027" + ) + declaration["growth"]["selector"]["period_value"] = 2027 + with pytest.raises(ValueError, match="resolve exactly once"): + resolve_reference_factor( + declaration, operand_facts, _document()["feed_identity"] + ) + + +def test_p1_high_rounds_once_to_1030467_and_old_truncation_refuses(operand_facts): + declaration = _entry("p1_ty2026_to_ty2027_high")["declaration"] + assert declaration["source_period"] == 2026 + assert [ + row["selector"]["period_value"] for row in declaration["source_schedule"] + ] == ["2025-Q2", "2025-Q3", "2025-Q4", "2026-Q1"] + declaration["expected_operational_factor"]["numerator"] = 1030466 + with pytest.raises(ValueError, match="expected factor mismatch"): + resolve_reference_factor( + declaration, operand_facts, _document()["feed_identity"] + ) + + +def test_root_enclosure_certifies_no_rounded_quarters_and_irrational_root(): + for factor_id in list(_EXPECTED)[:6]: + entry = _entry(factor_id) + receipt = entry["receipt"] + delta = Fraction(entry["declaration"]["growth"]["scenario_delta"]) + radicand = 1 + (Fraction("2.7") + delta) / 100 + lower = _fraction(receipt["root_bounds"]["lower"]) + upper = _fraction(receipt["root_bounds"]["upper"]) + assert lower**4 < radicand < upper**4 + assert receipt["root_bounds"]["exact"] is False + denominator = _fraction(receipt["denominator_mean"]) + assert _fraction(receipt["numerator_mean_bounds"]["lower"]) == ( + Fraction("1700.68") * sum(lower**i for i in range(4)) / 4 + ) + for bound in ("lower", "upper"): + value = _fraction(receipt["unrounded_factor_bounds"][bound]) + assert ( + value + == _fraction(receipt["numerator_mean_bounds"][bound]) / denominator + ) + assert round(value * 1000000) == _EXPECTED[factor_id] + + +def test_september_observation_reanchors_and_requires_reviewed_redeclaration( + operand_facts, +): + declaration = _entry()["declaration"] + anchor = next( + fact for fact in operand_facts if fact["period"]["value"] == "2026-Q2" + ) + september = copy.deepcopy(anchor) + september["aggregate_fact_key"] = "ledger.aggregate_fact.v2:synthetic_september" + september["period"]["value"] = "2026-Q3" + september["layout"]["record_set_id"] = "stats_nz.qes.awe.2026q3" + september["period_coverage"].update( + {"start_date": "2026-07-01", "end_date": "2026-09-30"} + ) + september["value"] = LexicalFloat("1750.00") + declaration["feed_identity"]["facts_sha256"] = "9" * 64 + facts = (*operand_facts, september) + with pytest.raises(ValueError, match="expected factor mismatch.*re-declaration"): + resolve_reference_factor(declaration, facts, declaration["feed_identity"]) + with localcontext() as context: + context.prec = 80 + q = (Decimal("1.027").sqrt()).sqrt() + numerator = (Decimal("1700.68") + Decimal(1750) * (1 + q + q * q)) / 4 + expected = round(numerator / Decimal("1537.7225") * 1000000) + declaration["expected_operational_factor"]["numerator"] = expected + factor, receipt = resolve_reference_factor( + declaration, facts, declaration["feed_identity"] + ) + assert factor["numerator"] == expected + assert [slot["status"] for slot in receipt["target_schedule"]] == [ + "observed", + "observed", + "projected", + "projected", + ] + lower = _fraction(receipt["root_bounds"]["lower"]) + assert ( + _fraction(receipt["numerator_mean_bounds"]["lower"]) + == (Fraction("1700.68") + 1750 * (1 + lower + lower**2)) / 4 + ) + for slot in receipt["target_schedule"][2:]: + assert slot["projection_anchor"] == { + "aggregate_fact_key": september["aggregate_fact_key"] + } diff --git a/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py b/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py index 7e9fba0e58..3a561ac84c 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py @@ -158,7 +158,11 @@ from hypothesis import assume, given, settings from hypothesis import strategies as st +from microcosm.build.chronicle_epoch import ( + is_accepted_consumer_artifact_schema_version, +) from microcosm.build.country_spec import load_country_spec +from microcosm.build.ledger_artifact import chronicle_feed_identity from microcosm.build.ledger_targets import ( LedgerTargetReference, compile_ledger_target_references, @@ -305,7 +309,8 @@ def rulespec_commit_mentions(value: object) -> set[str]: "rulespec" in any case); - the value, at any depth, of a key that contains "commit" in any case, unless the key's mapping is a foreign record (its repository - attribution lacks "rulespec") and neither the key nor an enclosing + attribution lacks "rulespec", or a complete Chronicle identity below + ``feed_identity``) and neither the key nor an enclosing key contains "rulespec". A foreign record cancels the context it sits in, except that its commit @@ -316,7 +321,13 @@ def rulespec_commit_mentions(value: object) -> set[str]: found: set[str] = set() - def visit(node: object, *, rulespec_context: bool, commit_value: bool) -> None: + def visit( + node: object, + *, + rulespec_context: bool, + commit_value: bool, + feed_identity_record: bool = False, + ) -> None: if isinstance(node, Mapping): repositories = [ repository @@ -327,7 +338,17 @@ def visit(node: object, *, rulespec_context: bool, commit_value: bool) -> None: rulespec_record = any( "rulespec" in repository.lower() for repository in repositories ) - foreign = named and not rulespec_record + artifact_identity = False + if feed_identity_record and is_accepted_consumer_artifact_schema_version( + node.get("artifact_schema_version") + ): + try: + chronicle_feed_identity(node) + except ValueError: + pass + else: + artifact_identity = True + foreign = (named or artifact_identity) and not rulespec_record if rulespec_record: commit_value = True elif foreign: @@ -344,6 +365,7 @@ def visit(node: object, *, rulespec_context: bool, commit_value: bool) -> None: child, rulespec_context=rulespec_context or "rulespec" in name, commit_value=commit_value or commit_key or "rulespec" in name, + feed_identity_record=name == "feed_identity", ) elif isinstance(node, list): for child in node: @@ -1193,7 +1215,12 @@ def test_each_set_is_a_self_identifying_nz_reference_resource( assert document["country"] == "nz" assert document["reference_set"] == reference_set expected_operations = ( - ["identity", "rolled_forward_by_ratio", "allocated_residual_by_ratio"] + [ + "identity", + "rolled_forward_by_ratio", + "allocated_residual_by_ratio", + "linear_combination", + ] if reference_set == "calibration" else ["identity"] ) @@ -1452,6 +1479,43 @@ def test_gate_profile_requires_exactly_the_calibration_families(self) -> None: class TestRulespecPin: + @pytest.mark.parametrize( + "case", + [ + "canonical", + "unattributed", + "incomplete", + "explicit_commit", + "prose", + "rulespec_context", + "wrong_schema", + ], + ) + def test_chronicle_identity_scope_preserves_rulespec_mentions(self, case) -> None: + identity = _load("reference_aging.json")["feed_identity"] + commit = identity["source_commit"] + payload = {"feed_identity": identity} + expected = set() + if case == "unattributed": + payload = {"other": identity} + expected = {commit} + elif case == "incomplete": + identity.pop("manifest_sha256") + expected = {commit} + elif case == "explicit_commit": + identity["rulespec_nz_commit"] = "c0ffee1" + expected = {"c0ffee1"} + elif case == "prose": + identity["notes"] = "RuleSpec-nz deadbee" + expected = {"deadbee"} + elif case == "rulespec_context": + payload = {"rulespec": payload} + expected = {commit} + elif case == "wrong_schema": + identity["artifact_schema_version"] = "unknown" + expected = {commit} + assert rulespec_commit_mentions(payload) == expected + def test_every_rulespec_commit_named_anywhere_was_reviewed_on_main(self) -> None: pin = _load("axiom_rules_bindings.json")["rulespec"] mentions = rulespec_commit_mentions(_package_payloads()) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_nz_target_references.py b/packages/microcosm-build/tests/engine_free/shared/test_nz_target_references.py index 004785b9e9..3c6ebed1c1 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_nz_target_references.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_nz_target_references.py @@ -111,6 +111,7 @@ def _effective_rows(profile="mar"): _document("target_references.json"), _document("reference_profiles.json"), {"census_residual_allocation": profile, "aging_index": "central"}, + factor_context=_document("reference_aging.json"), ) return document["target_references"] @@ -202,6 +203,11 @@ def test_residual_profiles_have_exact_recipient_rosters_and_preserve_other_targe if "households_by_size" in row["name"] } == {*REGIONS, "99"} for before, after in zip(raw, effective, strict=True): + if before["family"] == "income": + assert ( + after["metadata"]["activation_status"] == "requires_prerequisites" + ) + continue if ( before["metadata"]["activation_status"] != "requires_reference_profile_resolution" @@ -261,6 +267,7 @@ def test_full_nz_resource_resolves_directly_to_the_strict_compiler_document(prof raw, _document("reference_profiles.json"), {"census_residual_allocation": profile, "aging_index": "central"}, + factor_context=_document("reference_aging.json"), ) assert intervals is None assert set(effective) == set(raw) & REFERENCE_DOCUMENT_KEYS @@ -333,6 +340,85 @@ def test_v4_changes_only_the_residual_knob_and_prior_variants_keep_their_meaning assert knobs["census_residual_allocation"] == ( "small_households" if scenario["id"] == "V4" else "mar" ) + assert knobs["aging_index"] == {"V1": "low", "V2": "high"}.get( + scenario["id"], "central" + ) + + +@pytest.mark.parametrize("profile", ["low", "central", "high"]) +def test_ageing_profiles_have_237_explicit_companions_and_keep_b4_parked(profile): + profiles = _document("reference_profiles.json") + (transformation,) = profiles["dimensions"]["aging_index"]["profiles"][profile][ + "transformations" + ] + expected_counts = [ + f"ird_taxable_income_people_by_band__row_{row}" for row in range(8, 245) + ] + expected_money = [ + f"ird_taxable_income_total_by_band__row_{row}" for row in range(8, 245) + ] + assert transformation["count_reference_names"] == expected_counts + assert transformation["reference_names"] == expected_money + assert transformation["interval_table_ids"] == [] + assert transformation["factor_id"] == f"ird_ty2024_to_ty2027_{profile}" + assert not any("246" in name for name in expected_counts + expected_money) + base = _document("target_references.json") + effective, _, receipt = resolve_reference_profiles( + base, + profiles, + {"census_residual_allocation": "mar", "aging_index": profile}, + factor_context=_document("reference_aging.json"), + ) + before = {row["name"]: row for row in base["target_references"]} + after = {row["name"]: row for row in effective["target_references"]} + factor = {"low": 1.115056, "central": 1.117109, "high": 1.119158}[profile] + for count_name, money_name in zip(expected_counts, expected_money, strict=True): + for field in ("ledger_selector", "measure", "filter", "period"): + assert after[count_name][field] == before[count_name][field] + assert after[count_name].get("value_operation") == before[count_name].get( + "value_operation" + ) + assert after[count_name].get("value_operands") == before[count_name].get( + "value_operands" + ) + assert after[money_name]["value_operation"] == "linear_combination" + assert after[money_name]["value_operands"] == [ + { + "aggregate_fact_key": before[money_name]["ledger_selector"][ + "aggregate_fact_key" + ], + "expected_member_count": 1, + "weight": factor, + } + ] + for name in (count_name, money_name): + metadata = after[name]["metadata"] + assert metadata["activation_status"] == "requires_prerequisites" + assert json.loads(metadata["reference_prerequisites_remaining"]) == [ + "measurement:ird_taxable_income_ty2027_v1" + ] + assert receipt["selection"]["aging_index"] == profile + + +def test_resolved_money_uses_the_verified_factor_once_and_count_values_are_unchanged( + artifact, facts_by_key +): + rows = [row for row in _effective_rows() if row["family"] == "income"] + # This numerical operation check supplies activation only to its copies; + # the committed and resolved declarations remain parked on B4. + executable = copy.deepcopy(rows) + for row in executable: + row["metadata"]["activation_status"] = "active" + registry = compile_ledger_target_references( + artifact.facts, _parsed(executable), country="nz" + ) + assert len(registry.specs) == 474 + for row, target in zip(rows, registry.specs, strict=True): + source = facts_by_key[row["ledger_selector"]["aggregate_fact_key"]]["value"] + if row["metadata"]["reference_value_kind"] == "count": + assert target.value == source + else: + assert target.value == pytest.approx(source * 1.117109, rel=1e-12) @pytest.mark.parametrize("profile", ["mar", "small_households"]) @@ -555,7 +641,12 @@ def test_grouped_tenure_family_multiplicities_and_parked_definitions(): == "requires_reference_profile_resolution" ) if row["family"] == "income": - assert row["metadata"]["activation_status"].startswith("requires_g8r2") + assert row["metadata"]["activation_status"] == "requires_prerequisites" + assert json.loads(row["metadata"]["reference_prerequisites"]) == [ + "reference_profile:aging_index", + "measurement:ird_taxable_income_ty2027_v1", + ] + assert row["metadata"]["parking_reason"].startswith("B4:") def test_receipt_snapshots_are_split_and_stratified_inputs_stay_parked(): diff --git a/packages/microcosm-build/tests/engine_free/shared/test_reference_factors.py b/packages/microcosm-build/tests/engine_free/shared/test_reference_factors.py new file mode 100644 index 0000000000..73f3a1c0ae --- /dev/null +++ b/packages/microcosm-build/tests/engine_free/shared/test_reference_factors.py @@ -0,0 +1,535 @@ +"""Exact factor arithmetic, evidence and replay without country assumptions.""" + +from copy import deepcopy +from decimal import Decimal +from fractions import Fraction + +import pytest +from hypothesis import given, settings +from hypothesis import strategies as st +from microcosm_build.reference_factors import factor_context_fixture, factor_fixture + +from microcosm.build.ledger_artifact import LexicalFloat +from microcosm.build.reference_factors import ( + certified_nth_root_bounds, + factor_document_sha256, + replay_reference_factor, + resolve_reference_factor, + validate_factor_context, +) + + +def _fraction(row): + return Fraction(row["numerator"], row["denominator"]) + + +def _oracle_root(radicand, degree, places=50): + """Independent integer bisection oracle; no production arithmetic helpers.""" + denominator = 10**places + low, high = ( + 0, + max(2, (radicand.numerator // radicand.denominator + 2)) * denominator, + ) + while high - low > 1: + middle = (low + high) // 2 + if ( + middle**degree * radicand.denominator + <= radicand.numerator * denominator**degree + ): + low = middle + else: + high = middle + return Fraction(low, denominator), Fraction(high, denominator) + + +def _oracle_round(number, digits): + scaled = number * 10**digits + quotient, remainder = divmod(scaled.numerator, scaled.denominator) + if remainder * 2 > scaled.denominator or ( + remainder * 2 == scaled.denominator and quotient % 2 + ): + quotient += 1 + return quotient + + +def _oracle_factor(source, anchor, growth, frequency, digits): + low, high = _oracle_root(1 + Fraction(growth) / 100, frequency) + lower = ( + Fraction(anchor) + / Fraction(source) + * sum(low**index for index in range(frequency)) + / frequency + ) + upper = ( + Fraction(anchor) + / Fraction(source) + * sum(high**index for index in range(frequency)) + / frequency + ) + assert _oracle_round(lower, digits) == _oracle_round(upper, digits) + return _oracle_round(lower, digits), (low, high) + + +def test_factor_resolves_exact_operands_once_and_replays(): + context, facts, identity = factor_context_fixture() + entry = context["factors"][0] + receipt = entry["receipt"] + assert entry["operational_factor"] == {"numerator": 1100000, "denominator": 1000000} + assert _fraction(receipt["denominator_mean"]) == 100 + assert receipt["root_bounds"]["exact"] is True + assert [slot["status"] for slot in receipt["target_schedule"]] == [ + "observed", + "projected", + ] + assert all( + slot["numeric_token_proof"]["decimal_values_equal"] + for slot in receipt["source_schedule"] + ) + replay_reference_factor( + entry["declaration"], facts, identity, entry["operational_factor"], receipt + ) + assert validate_factor_context(context, ["toy_factor"]) == {"toy_factor": entry} + + +def test_factor_resolution_and_replay_ignore_caller_feed_pin_prose(): + declaration, facts, identity = factor_fixture() + factor, receipt = resolve_reference_factor(declaration, facts, identity) + pin = { + **identity, + "build": "Reviewed prose edit", + "source_repo": "Reviewed repository description", + } + + revised_factor, revised_receipt = resolve_reference_factor(declaration, facts, pin) + + assert revised_factor == factor + assert revised_receipt == receipt + assert revised_receipt["feed_identity"] == identity + assert declaration["feed_identity"] == identity + replay_reference_factor(declaration, facts, pin, factor, receipt) + + +@pytest.mark.parametrize("container", ["declaration", "context"]) +@pytest.mark.parametrize( + "field", + [ + "source_commit", + "facts_sha256", + "manifest_sha256", + "artifact_schema_version", + "consumer_fact_schema_versions", + "consumer_fact_schema_sha256", + "fact_row_count", + ], +) +def test_factor_identity_requires_every_authenticated_field(container, field): + context, facts, identity = factor_context_fixture() + declaration = context["factors"][0]["declaration"] + document = declaration if container == "declaration" else context + document["feed_identity"] = { + key: value for key, value in document["feed_identity"].items() if key != field + } + + with pytest.raises(ValueError, match="missing fields"): + if container == "declaration": + resolve_reference_factor(declaration, facts, identity) + else: + validate_factor_context(context) + + +@pytest.mark.parametrize("container", ["declaration", "context"]) +@pytest.mark.parametrize("field", ["build", "source_repo"]) +def test_factor_declared_identity_refuses_prose_fields(container, field): + context, facts, identity = factor_context_fixture() + declaration = context["factors"][0]["declaration"] + document = declaration if container == "declaration" else context + document["feed_identity"] = {**document["feed_identity"], field: "Prose"} + + with pytest.raises(ValueError, match="Unknown factor .*feed identity fields"): + if container == "declaration": + resolve_reference_factor(declaration, facts, identity) + else: + validate_factor_context(context) + + +def test_irrational_root_is_an_enclosure_never_an_exact_rational(): + expected, _ = _oracle_factor("100", "110", "3", 4, 6) + declaration, facts, identity = factor_fixture( + frequency=4, growth="3", expected_numerator=expected + ) + _, receipt = resolve_reference_factor(declaration, facts, identity) + bounds = receipt["root_bounds"] + lower, upper = _fraction(bounds["lower"]), _fraction(bounds["upper"]) + assert bounds["exact"] is False + assert lower < upper + assert lower**4 < Fraction(103, 100) < upper**4 + factor_bounds = receipt["unrounded_factor_bounds"] + assert _oracle_round(_fraction(factor_bounds["lower"]), 6) == expected + assert _oracle_round(_fraction(factor_bounds["upper"]), 6) == expected + + +def test_exact_root_is_recognized_without_false_irrationality(): + lower, upper, exact = certified_nth_root_bounds(Fraction(121, 100), 2, 8) + assert lower == upper == Fraction(11, 10) + assert exact is True + + +@pytest.mark.parametrize( + "anchor,expected", [("2469135", 1234568), ("2469133", 1234566)] +) +def test_single_final_rounding_uses_nearest_half_even(anchor, expected): + declaration, facts, identity = factor_fixture( + frequency=1, + source_level="2000000", + anchor_level=anchor, + expected_numerator=expected, + ) + factor, _ = resolve_reference_factor(declaration, facts, identity) + assert factor["numerator"] == expected + + +def test_observed_only_diagnostic_needs_no_projection_operand(): + declaration, facts, identity = factor_fixture() + observed = deepcopy(facts[-2]) + observed["aggregate_fact_key"] = "target-1" + observed["period"]["value"] = declaration["target_schedule"][1]["selector"][ + "period_value" + ] + observed["period_coverage"] = declaration["target_schedule"][1]["validation"][ + "period_coverage" + ] + facts.append(observed) + declaration["purpose"] = "diagnostic" + declaration["growth"] = None + factor, receipt = resolve_reference_factor(declaration, facts, identity) + assert factor["numerator"] == 1100000 + assert receipt["growth"] is None + assert all(slot["status"] == "observed" for slot in receipt["target_schedule"]) + + +@pytest.mark.parametrize( + "field,mutation,message", + [ + ("operation", "expression_eval", "operation"), + ("operation_version", 2, "operation"), + ("schema_version", 2, "schema_version"), + ("unknown", "field", "Unknown"), + ("subperiods_per_year", True, "subperiods_per_year"), + ("subperiods_per_year", 0, "subperiods_per_year"), + ("projection_policy", "june_anchor", "projection policy"), + ("purpose", "execute", "purpose"), + ("source_period", True, "period labels"), + ("target_period", None, "period labels"), + ("source_period", "", "period labels"), + ], +) +def test_closed_declaration_guards(field, mutation, message): + declaration, facts, identity = factor_fixture() + declaration[field] = mutation + with pytest.raises(ValueError, match=message): + resolve_reference_factor(declaration, facts, identity) + + +@pytest.mark.parametrize( + "mutation,message", + [ + ("missing", "exactly once"), + ("duplicate", "Duplicate"), + ("unit", "unit mismatch"), + ("source_sha256", "source_sha256 mismatch"), + ("source_vintage", "source_vintage mismatch"), + ("entity_role", "entity_role mismatch"), + ("coverage", "coverage mismatch"), + ("boolean", "Boolean"), + ("nan", "finite"), + ("float_without_token", "lexical numeric token"), + ("lossy_float", "does not reproduce"), + ("negative", "positive"), + ], +) +def test_operand_guards(mutation, message): + declaration, facts, identity = factor_fixture() + if mutation == "missing": + facts.pop(0) + elif mutation == "duplicate": + duplicate = deepcopy(facts[0]) + duplicate["aggregate_fact_key"] += "-duplicate" + facts.append(duplicate) + elif mutation == "unit": + facts[0]["observed_measure"]["unit"] = "other" + elif mutation == "source_sha256": + facts[0]["source"]["source_sha256"] = "b" * 64 + elif mutation == "source_vintage": + facts[0]["source"]["vintage"] = "revised" + elif mutation == "entity_role": + facts[0]["entity"]["role"] = "other" + elif mutation == "coverage": + facts[0]["period_coverage"]["end_date"] = "2001-02-01" + # The fixture shares this dictionary with its declaration; keep the pin. + declaration["source_schedule"][0]["validation"]["period_coverage"][ + "end_date" + ] = "2001-01-01" + facts[0]["period_coverage"] = { + "start_date": "2001-01-01", + "end_date": "2001-02-01", + } + elif mutation == "boolean": + facts[0]["value"] = True + elif mutation == "nan": + facts[0]["value"] = Decimal("NaN") + elif mutation == "float_without_token": + facts[0]["value"] = 100.0 + elif mutation == "lossy_float": + facts[0]["value"] = LexicalFloat("100.000000000000000001") + elif mutation == "negative": + facts[0]["value"] = Decimal("-1") + with pytest.raises(ValueError, match=message): + resolve_reference_factor(declaration, facts, identity) + + +def test_lexical_float_conversion_is_proved_and_used_exactly(): + declaration, facts, identity = factor_fixture( + source_level="100.25", anchor_level="110.275", expected_numerator=1100000 + ) + for fact in facts: + fact["value"] = LexicalFloat(str(fact["value"])) + _, receipt = resolve_reference_factor(declaration, facts, identity) + proof = receipt["source_schedule"][0]["numeric_token_proof"] + assert proof == { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "100.25", + "recovered_decimal": "100.25", + "decimal_values_equal": True, + } + assert _fraction(receipt["denominator_mean"]) == Fraction(401, 4) + + +def test_integer_operands_are_exact_beyond_binary_float_precision(): + source = 10**80 + 10 + anchor = source * 11 // 10 + declaration, facts, identity = factor_fixture( + source_level=str(source), + anchor_level=str(anchor), + expected_numerator=1100000, + ) + for fact in facts: + fact["value"] = int(fact["value"]) + + factor, receipt = resolve_reference_factor(declaration, facts, identity) + + assert factor == {"numerator": 1100000, "denominator": 1000000} + assert _fraction(receipt["denominator_mean"]) == source + assert _fraction(receipt["numerator_mean_bounds"]["lower"]) == anchor + assert _fraction(receipt["numerator_mean_bounds"]["upper"]) == anchor + operands = [ + *receipt["source_schedule"], + *(slot for slot in receipt["target_schedule"] if slot["status"] == "observed"), + receipt["growth"], + ] + for operand in operands: + token = operand["decimal_operand"] + assert operand["numeric_token_proof"] == { + "numeric_representation": "exact_integer", + "lexical_token": token, + "recovered_decimal": token, + "decimal_values_equal": True, + } + assert receipt["source_schedule"][0]["decimal_operand"] == str(source) + assert receipt["target_schedule"][0]["decimal_operand"] == str(anchor) + + +def test_source_projection_requires_existing_allow_guard_even_with_exact_key(): + declaration, facts, identity = factor_fixture() + declaration["growth"]["assertion_policy"] = "observed_only" + with pytest.raises(ValueError, match="allow_source_projection"): + resolve_reference_factor(declaration, facts, identity) + + +@pytest.mark.parametrize( + "mutation,message", + [ + ("schedule_order", "misordered"), + ("duplicate_period", "Duplicate"), + ("schedule_gap", "noncontiguous"), + ("negative_weight", "positive"), + ("offset", "projection offsets"), + ("unknown_selector", "selector"), + ("growth_floor", "positive 1"), + ("sensitivity", "sensitivity provenance"), + ("expired", "expired"), + ("wrong_expected", "expected factor mismatch"), + ("wrong_feed", "feed identity"), + ("wrong_growth_period", "exactly once"), + ("source_column", "source_column_id mismatch"), + ], +) +def test_numeric_and_schedule_guards(mutation, message): + declaration, facts, identity = factor_fixture() + if mutation == "schedule_order": + declaration["source_schedule"].reverse() + elif mutation == "duplicate_period": + declaration["source_schedule"][1]["selector"]["period_value"] = "2001-01-01" + elif mutation == "schedule_gap": + declaration["source_schedule"][1]["validation"]["period_coverage"][ + "start_date" + ] = "2001-01-04" + elif mutation == "negative_weight": + declaration["source_schedule"][0]["weight"] = "-1" + elif mutation == "offset": + declaration["target_schedule"][1]["projection_offset"] = 0 + elif mutation == "unknown_selector": + declaration["source_schedule"][0]["selector"]["unit"] = "money_per_week" + elif mutation == "growth_floor": + facts[-1]["value"] = Decimal("-100") + elif mutation == "sensitivity": + declaration["growth"]["sensitivity_provenance"] = [] + elif mutation == "expired": + declaration["validity"]["valid_until"] = "2005-01-01" + elif mutation == "wrong_expected": + declaration["expected_operational_factor"]["numerator"] += 1 + elif mutation == "wrong_feed": + identity = {**identity, "facts_sha256": "wrong"} + elif mutation == "wrong_growth_period": + declaration["growth"]["selector"]["period_value"] = "2003" + elif mutation == "source_column": + declaration["growth"]["validation"]["source_column_id"] = "wrong" + with pytest.raises(ValueError, match=message): + resolve_reference_factor(declaration, facts, identity) + + +def test_observed_preferred_requires_redeclaration_and_reanchors_projection(): + declaration, facts, identity = factor_fixture(frequency=4) + observed = deepcopy(facts[-2]) + observed["aggregate_fact_key"] = "target-1" + observed["value"] = Decimal("120") + observed["period"]["value"] = declaration["target_schedule"][1]["selector"][ + "period_value" + ] + observed["period_coverage"] = declaration["target_schedule"][1]["validation"][ + "period_coverage" + ] + facts.append(observed) + with pytest.raises(ValueError, match="expected factor mismatch"): + resolve_reference_factor(declaration, facts, identity) + # Growth zero makes the latest-anchor formula transparent: (110+120+120+120)/4. + declaration["expected_operational_factor"]["numerator"] = 1175000 + _, receipt = resolve_reference_factor(declaration, facts, identity) + assert _fraction(receipt["numerator_mean_bounds"]["lower"]) == Fraction(470, 4) + assert [slot["projection_offset"] for slot in receipt["target_schedule"]] == [ + 0, + 0, + 1, + 2, + ] + assert receipt["target_schedule"][2]["projection_anchor"] == { + "aggregate_fact_key": "target-1" + } + + +def test_observed_gap_refuses_instead_of_guessing_anchor(): + declaration, facts, identity = factor_fixture(frequency=4) + observed = deepcopy(facts[-2]) + observed["period"]["value"] = declaration["target_schedule"][2]["selector"][ + "period_value" + ] + observed["period_coverage"] = declaration["target_schedule"][2]["validation"][ + "period_coverage" + ] + facts.append(observed) + with pytest.raises(ValueError, match="contiguous from the anchor"): + resolve_reference_factor(declaration, facts, identity) + + +def test_self_consistent_receipt_cannot_replace_compile_replay(): + context, facts, identity = factor_context_fixture() + entry = context["factors"][0] + entry["declaration"]["expected_operational_factor"]["numerator"] = 1200000 + entry["operational_factor"]["numerator"] = 1200000 + entry["receipt"]["operational_factor"]["numerator"] = 1200000 + entry["receipt"]["declaration_sha256"] = factor_document_sha256( + entry["declaration"] + ) + entry["receipt"]["receipt_sha256"] = factor_document_sha256( + { + key: value + for key, value in entry["receipt"].items() + if key != "receipt_sha256" + } + ) + assert validate_factor_context(context, ["toy_factor"]) + with pytest.raises(ValueError, match="expected factor mismatch"): + replay_reference_factor( + entry["declaration"], + facts, + identity, + entry["operational_factor"], + entry["receipt"], + ) + + +def test_receipt_tampering_refuses_and_unselected_edits_are_local(): + context, _, _ = factor_context_fixture() + other = deepcopy(context["factors"][0]) + other["declaration"]["id"] = "unused" + other["receipt"]["receipt_sha256"] = "invalid" + context["factors"].append(other) + assert list(validate_factor_context(context, ["toy_factor"])) == ["toy_factor"] + with pytest.raises(ValueError, match="hash mismatch"): + validate_factor_context(context, ["unused"]) + + +@settings(max_examples=70, deadline=None) +@given( + source=st.integers(1, 10**9), + anchor=st.integers(1, 10**9), + growth=st.integers(-49, 49), + frequency=st.sampled_from([1, 2, 4, 12]), + digits=st.sampled_from([4, 6, 8]), +) +def test_certified_rounding_and_root_bounds_match_independent_fraction_oracle( + source, anchor, growth, frequency, digits +): + source_text, anchor_text = str(Decimal(source) / 100), str(Decimal(anchor) / 100) + expected, oracle_bounds = _oracle_factor( + source_text, anchor_text, str(growth), frequency, digits + ) + declaration, facts, identity = factor_fixture( + source_level=source_text, + anchor_level=anchor_text, + growth=str(growth), + frequency=frequency, + digits=digits, + expected_numerator=expected, + ) + if expected == 0: + with pytest.raises( + ValueError, match="Expected operational factor must be positive" + ): + resolve_reference_factor(declaration, facts, identity) + return + factor, receipt = resolve_reference_factor(declaration, facts, identity) + assert factor["numerator"] == expected + lower, upper = ( + _fraction(receipt["root_bounds"]["lower"]), + _fraction(receipt["root_bounds"]["upper"]), + ) + radicand = 1 + Fraction(growth, 100) + assert lower**frequency <= radicand <= upper**frequency + assert lower <= oracle_bounds[1] and upper >= oracle_bounds[0] + if receipt["root_bounds"]["exact"]: + assert lower == upper and lower**frequency == radicand + else: + assert lower < upper + + +def test_very_large_integer_cross_products_do_not_overflow(): + huge = 10**200 + declaration, facts, identity = factor_fixture( + source_level=str(huge), + anchor_level=str(3 * huge), + frequency=12, + expected_numerator=3000000, + ) + factor, receipt = resolve_reference_factor(declaration, facts, identity) + assert factor["numerator"] == 3000000 + assert _fraction(receipt["denominator_mean"]) == huge diff --git a/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py b/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py index c7324e58d7..c803dc5bb9 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py @@ -3,8 +3,12 @@ from __future__ import annotations import copy +import json import pytest +from hypothesis import given, settings +from hypothesis import strategies as st +from microcosm_build.reference_factors import factor_context_fixture from microcosm.build.reference_profiles import ( REFERENCE_DOCUMENT_KEYS, @@ -291,3 +295,316 @@ def test_effective_document_projects_resource_headers_and_binds_full_base(): effective ) assert validate_reference_resolution(effective, receipt) == receipt + + +def _scaling_inputs(pair_count=2, anchor_level="110"): + context, _, _ = factor_context_fixture(anchor_level=anchor_level) + references = {"country": "toy", "target_references": []} + money, counts = [], [] + for kind, names in (("count", counts), ("money", money)): + for index in range(pair_count): + name = f"{kind}_{index}" + names.append(name) + selector = _selector(f"{kind}-fact-{index}", index) + selector.update( + source_measure_id=kind, + source_concept=f"households.{kind}", + period_value=2001, + ) + row = { + "name": name, + "entity": "household", + "measure": f"prepared_{kind}", + "filter": f"band_{index}", + "period": 2002, + "ledger_selector": selector, + "metadata": { + "activation_status": "requires_prerequisites", + "reference_value_kind": kind, + "required_reference_profile": "aging_index", + "required_measurement_binding": "reviewed_income", + "reference_prerequisites": json.dumps( + ["reference_profile:aging_index", "measurement:reviewed_income"] + ), + "parking_reason": "Reviewed measurement remains unavailable.", + }, + } + if kind == "count": + row.update(value_operation="identity", value_operands=[]) + references["target_references"].append(row) + transformation = { + "operation": "scale_money_and_intervals", + "factor_id": "toy_factor", + "reference_names": money, + "count_reference_names": counts, + "interval_table_ids": [], + "required_measurement_binding": "reviewed_income", + } + declarations = { + "schema_version": 1, + "dimensions": { + "aging_index": { + "default": "central", + "profiles": { + name: {"transformations": [copy.deepcopy(transformation)]} + for name in ("low", "central", "high") + }, + } + }, + } + return references, declarations, context + + +def test_factor_scales_money_once_keeps_counts_and_measurement_prerequisite(): + references, declarations, context = _scaling_inputs() + original = copy.deepcopy((references, declarations, context)) + effective, _, receipt = resolve_reference_profiles( + references, declarations, {}, factor_context=context + ) + assert (references, declarations, context) == original + assert receipt["resolver_version"] == "reference-profiles-v2" + assert set(receipt["selected_factors"]) == {"toy_factor"} + for source, row in zip( + references["target_references"], effective["target_references"], strict=True + ): + metadata = row["metadata"] + assert metadata["activation_status"] == "requires_prerequisites" + assert metadata["parking_reason"] == source["metadata"]["parking_reason"] + assert json.loads(metadata["reference_prerequisites_remaining"]) == [ + "measurement:reviewed_income" + ] + assert row["ledger_selector"] == source["ledger_selector"] + if row["measure"] == "prepared_count": + assert {key: value for key, value in row.items() if key != "metadata"} == { + key: value for key, value in source.items() if key != "metadata" + } + assert receipt["transformations"][row["name"]]["operation"] == "identity" + else: + assert row["value_operation"] == "linear_combination" + assert row["value_operands"] == [ + { + "aggregate_fact_key": source["ledger_selector"][ + "aggregate_fact_key" + ], + "expected_member_count": 1, + "weight": 1.1, + } + ] + assert validate_reference_resolution(effective, receipt) == receipt + with pytest.raises(ValueError, match="already been resolved"): + resolve_reference_profiles(effective, declarations, {}, factor_context=context) + + +@given( + pair_count=st.integers(min_value=1, max_value=16), + anchor=st.integers(min_value=1, max_value=500), +) +@settings(max_examples=16, database=None, deadline=None) +def test_counts_are_invariant_and_every_money_row_uses_one_common_factor( + pair_count, anchor +): + references, declarations, context = _scaling_inputs(pair_count, str(anchor)) + effective, _, _ = resolve_reference_profiles( + references, declarations, {}, factor_context=context + ) + counts = references["target_references"][:pair_count] + effective_counts = effective["target_references"][:pair_count] + for before, after in zip(counts, effective_counts, strict=True): + assert before["ledger_selector"] == after["ledger_selector"] + assert before["value_operation"] == after["value_operation"] + assert before["value_operands"] == after["value_operands"] + weights = [ + row["value_operands"] for row in effective["target_references"][pair_count:] + ] + assert all(len(operands) == 1 for operands in weights) + rational = context["factors"][0]["operational_factor"] + assert {operands[0]["weight"] for operands in weights} == { + rational["numerator"] / rational["denominator"] + } + with pytest.raises(ValueError, match="already been resolved"): + resolve_reference_profiles(effective, declarations, {}, factor_context=context) + + +@pytest.mark.parametrize( + "mutation", + [ + "missing_count", + "swapped_roles", + "missing_both", + "mismatched_filter", + "overlap", + "duplicate", + "source_period", + "target_period", + "diagnostic_factor", + "money_already_scaled", + "arbitrary_parking", + "missing_prerequisites", + "wrong_measurement", + "override_prerequisites", + "override_measurement", + "missing_context", + "wrong_factor", + "wrong_receipt", + "wrong_profile_factor", + ], +) +def test_invalid_factor_profiles_and_companions_refuse(mutation): + references, declarations, context = _scaling_inputs() + transform = declarations["dimensions"]["aging_index"]["profiles"]["central"][ + "transformations" + ][0] + if mutation == "missing_count": + transform["count_reference_names"].pop() + elif mutation == "swapped_roles": + transform["count_reference_names"], transform["reference_names"] = ( + transform["reference_names"], + transform["count_reference_names"], + ) + elif mutation == "missing_both": + transform["count_reference_names"].pop() + transform["reference_names"].pop() + elif mutation == "mismatched_filter": + references["target_references"][0]["filter"] = "wrong_band" + elif mutation == "overlap": + declarations["dimensions"]["another"] = copy.deepcopy( + declarations["dimensions"]["aging_index"] + ) + elif mutation == "duplicate": + transform["reference_names"].append(transform["reference_names"][0]) + elif mutation in {"source_period", "target_period"}: + key = "ledger_selector" if mutation == "source_period" else "period" + for row in references["target_references"]: + if key == "ledger_selector": + row[key]["period_value"] = 1999 + else: + row[key] = 1999 + elif mutation == "diagnostic_factor": + entry = context["factors"][0] + entry["declaration"]["purpose"] = "diagnostic" + from microcosm.build.reference_factors import factor_document_sha256 + + entry["receipt"]["declaration_sha256"] = factor_document_sha256( + entry["declaration"] + ) + body = { + key: value + for key, value in entry["receipt"].items() + if key != "receipt_sha256" + } + entry["receipt"]["receipt_sha256"] = factor_document_sha256(body) + elif mutation == "money_already_scaled": + row = references["target_references"][-1] + row["value_operation"] = "linear_combination" + row["value_operands"] = [ + { + "aggregate_fact_key": row["ledger_selector"]["aggregate_fact_key"], + "expected_member_count": 1, + "weight": 1.1, + } + ] + elif mutation == "arbitrary_parking": + references["target_references"][0]["metadata"]["activation_status"] = ( + "requires_arbitrary_evidence" + ) + elif mutation == "missing_prerequisites": + references["target_references"][0]["metadata"].pop("reference_prerequisites") + elif mutation == "wrong_measurement": + references["target_references"][0]["metadata"][ + "required_measurement_binding" + ] = "other" + elif mutation in {"override_prerequisites", "override_measurement"}: + key = ( + "reference_prerequisites" + if mutation == "override_prerequisites" + else "required_measurement_binding" + ) + transform["metadata"] = {key: "forged"} + elif mutation == "missing_context": + context = None + elif mutation == "wrong_factor": + context["factors"][0]["operational_factor"]["numerator"] += 1 + elif mutation == "wrong_receipt": + context["factors"][0]["receipt"]["receipt_sha256"] = "0" * 64 + elif mutation == "wrong_profile_factor": + transform["factor_id"] = "missing" + with pytest.raises(ValueError): + resolve_reference_profiles(references, declarations, {}, factor_context=context) + + +def test_nonempty_interval_roster_refuses_until_interval_implementation(): + references, declarations, _ = _scaling_inputs() + transform = declarations["dimensions"]["aging_index"]["profiles"]["central"][ + "transformations" + ][0] + transform["interval_table_ids"] = ["reviewed_table"] + with pytest.raises(ValueError, match="interval scaling lands in r2b"): + resolve_reference_profiles(references, declarations, {}) + + +def test_factor_evidence_and_unselected_profile_locality(): + references, declarations, context = _scaling_inputs() + low = resolve_reference_profiles( + references, declarations, {"aging_index": "low"}, factor_context=context + ) + central = resolve_reference_profiles( + references, declarations, {}, factor_context=context + ) + declarations["dimensions"]["aging_index"]["profiles"]["high"]["notes"] = ( + "edited unselected declaration" + ) + unselected = copy.deepcopy(context["factors"][0]) + unselected["declaration"]["id"] = "unselected_invalid_factor" + context["factors"].append(unselected) + assert ( + resolve_reference_profiles( + references, declarations, {"aging_index": "low"}, factor_context=context + ) + == low + ) + assert ( + resolve_reference_profiles(references, declarations, {}, factor_context=context) + == central + ) + # Factor evidence is part of selection identity, not merely an opaque attachment. + receipt = copy.deepcopy(central[2]) + receipt["selected_factor_identities"]["toy_factor"]["receipt_sha256"] = "0" * 64 + receipt.pop("receipt_sha256") + receipt["receipt_sha256"] = reference_document_sha256(receipt) + with pytest.raises(ValueError, match="factor evidence identity"): + validate_reference_resolution(central[0], receipt) + + +def test_legacy_receipt_remains_version_one_without_factor_fields(): + references, declarations = _inputs() + _, _, receipt = resolve_reference_profiles(references, declarations, {}) + assert receipt["resolver_version"] == "reference-profiles-v1" + assert "selected_factors" not in receipt + assert "selected_factor_identities" not in receipt + + +def test_unrelated_direct_fact_references_are_outside_companion_rosters(): + references, declarations, context = _scaling_inputs() + direct = { + "name": "other_direct_fact", + "ledger_fact_key": "other", + "entity": "household", + } + references["target_references"].append(direct) + effective, _, _ = resolve_reference_profiles( + references, declarations, {}, factor_context=context + ) + assert effective["target_references"][-1] == direct + + +def test_self_consistent_effective_money_tampering_still_refuses(): + references, declarations, context = _scaling_inputs() + effective, _, receipt = resolve_reference_profiles( + references, declarations, {}, factor_context=context + ) + effective["target_references"][-1]["value_operands"][0]["weight"] *= 1.1 + receipt["effective_references_sha256"] = reference_document_sha256(effective) + receipt.pop("receipt_sha256") + receipt["receipt_sha256"] = reference_document_sha256(receipt) + with pytest.raises(ValueError, match="must be applied exactly once"): + validate_reference_resolution(effective, receipt) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_country_bundles.py b/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_country_bundles.py index b61bccc13e..981c214de6 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_country_bundles.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_country_bundles.py @@ -233,6 +233,7 @@ def test_nz_generation_zero_views_come_from_the_country_spec_seam() -> None: "geography_spine.json", "holdout_references.json", "precal_references.json", + "reference_aging.json", "reference_inventory.json", "reference_profiles.json", "release_contract.json", diff --git a/packages/microcosm-build/tests/engine_free/shared/test_transport_composed_graph.py b/packages/microcosm-build/tests/engine_free/shared/test_transport_composed_graph.py index 051e1c4cb7..5d6ee3a416 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_transport_composed_graph.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_transport_composed_graph.py @@ -94,6 +94,9 @@ from microcosm.graph.canonical import canonical_json from microcosm.graph.keys import node_key, source_content_key from test_support.microcosm_build.chronicle_feed import write_pinned_artifact +from test_support.microcosm_build.reference_factor_transport import ( + factor_transport_fixture, +) from test_support.microcosm_build.transport_composed import ( FAMILY_INPUTS, PROGRAMS, @@ -964,6 +967,143 @@ def test_preflight_checks_the_selected_variant_reference_status(composed): validate_transport_activation(spec) +def test_factor_context_is_read_without_composition_feed_io(tmp_path, monkeypatch): + _, pin, _, _, spec = factor_transport_fixture(tmp_path) + spec["resources"]["chronicle_feed"] = pin + + def no_feed_io(*args, **kwargs): + raise AssertionError("Composition must not load the consumer artifact.") + + monkeypatch.setattr( + "microcosm.build.ledger_artifact.load_ledger_consumer_artifact", no_feed_io + ) + view = compose_module.reference_profile_view(spec) + assert view[2]["selected_factors"]["toy_factor"]["operational_factor"] == { + "numerator": 1100000, + "denominator": 1000000, + } + assert all( + row["metadata"]["activation_status"] == "requires_prerequisites" + for row in view[0]["target_references"] + ) + assert compose_module.transport_resource(spec, "target_references") == view[0] + + +def test_factor_context_ignores_feed_pin_prose(tmp_path): + _, pin, _, _, spec = factor_transport_fixture(tmp_path) + spec["resources"]["chronicle_feed"] = pin + before = compose_module.reference_profile_view(spec) + pin["build"] = "Reviewed prose edit" + pin["source_repo"] = "Reviewed repository description" + assert compose_module.reference_profile_view(spec) == before + + +@pytest.mark.parametrize( + "defect", + ["missing", "wrong_feed", "wrong_commit", "wrong_manifest", "wrong_factor"], +) +def test_preflight_refuses_missing_or_incompatible_default_factor_context( + tmp_path, defect +): + _, pin, _, _, spec = factor_transport_fixture(tmp_path) + spec["resources"]["chronicle_feed"] = pin + if defect == "missing": + spec["resources"].pop("factor_context") + elif defect == "wrong_feed": + spec["resources"]["chronicle_feed"] = {**pin, "facts_sha256": "0" * 64} + elif defect == "wrong_commit": + pin["source_commit"] = "d" * 40 + elif defect == "wrong_manifest": + pin["manifest_sha256"] = "0" * 64 + else: + spec["resources"]["factor_context"]["factors"][0]["operational_factor"][ + "numerator" + ] += 1 + with pytest.raises( + ValueError, match="missing resource|incompatible|factor differs" + ): + validate_transport_activation(spec) + + +def test_preflight_resolves_empty_knobs_only_once(tmp_path, monkeypatch): + _, _, _, _, spec = factor_transport_fixture(tmp_path) + original = compose_module.reference_profile_view + selections = [] + + def recording_view(spec, selection=None): + selections.append(selection) + return original(spec, selection) + + monkeypatch.setattr(compose_module, "reference_profile_view", recording_view) + with pytest.raises(ValueError, match="Transport spec is not activated"): + validate_transport_activation(spec) + assert selections + assert all(selection is None for selection in selections) + + +def test_preflight_validates_scenario_factor_context_before_activation(tmp_path): + _, _, _, _, spec = factor_transport_fixture(tmp_path) + profiles = spec["resources"]["reference_profiles"]["dimensions"]["index"][ + "profiles" + ] + profiles["low"]["transformations"][0]["factor_id"] = "missing-scenario-factor" + assert compose_module.reference_profile_view(spec)[2]["selection"] == { + "index": "central" + } + spec["resources"]["scenarios"]["scenarios"].append( + {"id": "variant", "knobs": {"index": "low"}} + ) + with pytest.raises(ValueError, match="Missing selected factor"): + validate_transport_activation(spec) + + +def test_declared_ageing_scenarios_bind_selected_evidence_and_preserve_locality(): + spec = load_transport_spec("nz") + profiles = spec["resources"]["reference_profiles"] + receipts = {} + for scenario in spec["resources"]["scenarios"]["scenarios"]: + receipt = compose_module.reference_profile_view(spec, scenario["knobs"])[2] + receipts[scenario["id"]] = receipt + profile = {"V1": "low", "V2": "high"}.get(scenario["id"], "central") + factor_id = f"ird_ty2024_to_ty2027_{profile}" + assert receipt["selection"] == { + "aging_index": profile, + "census_residual_allocation": ( + "small_households" if scenario["id"] == "V4" else "mar" + ), + } + assert set(receipt["selected_factors"]) == {factor_id} + entry = next( + entry + for entry in spec["resources"]["reference_aging"]["factors"] + if entry["declaration"]["id"] == factor_id + ) + assert receipt["selected_factors"][factor_id] == entry + chosen = { + dimension: { + "name": name, + "declaration": profiles["dimensions"][dimension]["profiles"][name], + } + for dimension, name in receipt["selection"].items() + } + assert ( + receipt["selected_declarations_sha256"] + == hashlib.sha256(canonical_json(chosen)).hexdigest() + ) + profiles["dimensions"]["aging_index"]["profiles"]["high"]["notes"] = ( + "Reviewed edit confined to the unselected high profile." + ) + for scenario in spec["resources"]["scenarios"]["scenarios"]: + revised = compose_module.reference_profile_view(spec, scenario["knobs"])[2] + if scenario["id"] == "V2": + assert ( + revised["reference_selection_sha256"] + != receipts["V2"]["reference_selection_sha256"] + ) + else: + assert revised == receipts[scenario["id"]] + + class _IllegalOwner(KernelBase): ref = "test.illegal_owner@1" capabilities = Capabilities(Determinism.DETERMINISTIC) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_transport_target_kernels.py b/packages/microcosm-build/tests/engine_free/shared/test_transport_target_kernels.py index 9550f1ccaf..426c3d88ab 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_transport_target_kernels.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_transport_target_kernels.py @@ -21,6 +21,7 @@ import json from dataclasses import replace +from pathlib import Path import numpy as np import pandas as pd @@ -28,6 +29,8 @@ from hypothesis import HealthCheck, given, settings from hypothesis import strategies as st +import microcosm.build.reference_factors as reference_factors_module +import microcosm.build.reference_profiles as reference_profiles_module from microcosm.build.ledger_artifact import ChronicleFeedPinMismatchError from microcosm.build.reference_profiles import ( reference_document_sha256, @@ -62,6 +65,9 @@ ) from microcosm.graph.canonical import canonical_json from test_support.microcosm_build.chronicle_feed import write_pinned_artifact +from test_support.microcosm_build.reference_factor_transport import ( + factor_transport_fixture, +) from test_support.microcosm_build.transport_graph import ( CALIBRATION_TARGETS, COUNTRY, @@ -167,6 +173,150 @@ def test_kernels_declare_honest_country_neutral_contracts() -> None: ) +@pytest.mark.parametrize( + "kernel", [TARGETS_COMPILE, TAKEUP_COMPARE], ids=lambda k: k.ref +) +def test_factor_verifier_source_changes_compilation_identity( + tmp_path, monkeypatch, kernel +): + before = kernel.implementation_hash() + source = Path(reference_factors_module.__file__) + changed = tmp_path / source.name + changed.write_bytes(source.read_bytes() + b"\n# verifier implementation mutation\n") + monkeypatch.setattr(reference_factors_module, "__file__", str(changed)) + assert kernel.implementation_hash() != before + + +def test_compiler_replays_factor_and_applies_one_common_money_weight(tmp_path): + path, pin, document, receipt, _ = factor_transport_fixture(tmp_path, activate=True) + payload, registry = compile_target_surface( + path, + document, + country=COUNTRY, + references_sha256=reference_document_sha256(document), + feed_pin=pin, + reference_resolution=receipt, + ) + assert registry.specs[0].value == 3 + assert registry.specs[1].value == pytest.approx(110) + assert decode_target_surface(payload).reference_resolution == receipt + + +def test_compile_replay_refuses_self_consistent_forged_factor(tmp_path): + path, pin, document, receipt, _ = factor_transport_fixture( + tmp_path, forged=True, activate=True + ) + # Composition has accepted all declaration/receipt hashes; only feed replay + # can distinguish this forged level from the authenticated source operand. + with pytest.raises(ValueError, match="expected factor mismatch"): + compile_target_surface( + path, + document, + country=COUNTRY, + references_sha256=reference_document_sha256(document), + feed_pin=pin, + reference_resolution=receipt, + ) + + +def test_compile_replays_correct_factor_without_pin_when_manifest_binds_commit( + tmp_path, +): + path, _, document, receipt, _ = factor_transport_fixture( + tmp_path, activate=True, manifest_commit=True + ) + _, registry = compile_target_surface( + path, + document, + country=COUNTRY, + references_sha256=reference_document_sha256(document), + reference_resolution=receipt, + ) + assert registry.specs[1].value == pytest.approx(110) + + +@pytest.mark.parametrize("manifest_commit", [False, True]) +def test_compile_without_pin_refuses_unauthenticated_or_different_feed( + tmp_path, manifest_commit +): + path, _, document, receipt, _ = factor_transport_fixture( + tmp_path, activate=True, manifest_commit=manifest_commit + ) + if manifest_commit: + # The declaration still has a valid receipt for its original artifact. + manifest_path = path / "manifest.json" + manifest = json.loads(manifest_path.read_text()) + manifest["source_commit"] = "d" * 40 + manifest_path.write_text(json.dumps(manifest)) + with pytest.raises(ValueError, match="cannot be authenticated|manifest_sha256"): + compile_target_surface( + path, + document, + country=COUNTRY, + references_sha256=reference_document_sha256(document), + reference_resolution=receipt, + ) + + +def test_compile_factor_ignores_pin_prose(tmp_path): + path, pin, document, receipt, _ = factor_transport_fixture(tmp_path, activate=True) + arguments = dict( + country=COUNTRY, + references_sha256=reference_document_sha256(document), + reference_resolution=receipt, + ) + before, _ = compile_target_surface(path, document, feed_pin=pin, **arguments) + after, _ = compile_target_surface( + path, document, feed_pin={**pin, "build": "Reviewed prose edit"}, **arguments + ) + assert before == after + + +@pytest.mark.parametrize("field", ["source_commit", "facts_sha256", "manifest_sha256"]) +def test_compile_factor_refuses_changed_pin_identity(tmp_path, field): + path, pin, document, receipt, _ = factor_transport_fixture(tmp_path, activate=True) + pin[field] = "d" * len(pin[field]) + with pytest.raises(ValueError, match="feed identity mismatch|pin mismatch"): + compile_target_surface( + path, + document, + country=COUNTRY, + references_sha256=reference_document_sha256(document), + feed_pin=pin, + reference_resolution=receipt, + ) + + +def test_compile_replay_refuses_tampered_evidence_with_recomputed_hashes(tmp_path): + path, pin, document, receipt, _ = factor_transport_fixture(tmp_path, activate=True) + factor_receipt = receipt["selected_factors"]["toy_factor"]["receipt"] + factor_receipt["source_schedule"][0]["decimal_operand"] = "99" + factor_receipt.pop("receipt_sha256") + factor_receipt["receipt_sha256"] = reference_document_sha256(factor_receipt) + receipt["selected_factor_identities"]["toy_factor"]["receipt_sha256"] = ( + factor_receipt["receipt_sha256"] + ) + receipt["reference_selection_sha256"] = reference_document_sha256( + reference_profiles_module._selection_identity(receipt) + ) + for row in document["target_references"]: + row["metadata"]["reference_selection_sha256"] = receipt[ + "reference_selection_sha256" + ] + receipt["effective_references_sha256"] = reference_document_sha256(document) + receipt.pop("receipt_sha256") + receipt["receipt_sha256"] = reference_document_sha256(receipt) + with pytest.raises(ValueError, match="Factor replay differs"): + compile_target_surface( + path, + document, + country=COUNTRY, + references_sha256=reference_document_sha256(document), + feed_pin=pin, + reference_resolution=receipt, + ) + + def test_compile_emits_a_traced_surface_bound_to_facts_not_paths(tmp_path) -> None: payload = _surface(tmp_path) surface = decode_target_surface(payload) diff --git a/packages/microcosm-build/tests/golden/nz_country_spec.json b/packages/microcosm-build/tests/golden/nz_country_spec.json index 975b622934..056f151afc 100644 --- a/packages/microcosm-build/tests/golden/nz_country_spec.json +++ b/packages/microcosm-build/tests/golden/nz_country_spec.json @@ -1,6 +1,6 @@ { "country": "nz", - "fingerprint": "d970b6e900b7f5d455a86158718703444ac91c15d49b2203720fc2bdf2baba7d", + "fingerprint": "be277bbabfb0c40601a9719aa5f3adeb5b51c10e8adcc9dfad36475b7f35d425", "gate_ids": [ "calibration_per_family_fit", "demography_per_family_fit", @@ -45,15 +45,16 @@ "benefit_unit_rule.json": "a9e8cbfd38a64d2f8db0002fe8c31bb5a18fb34dbe59fa943336487ce95e2ca8", "calibration_bindings.json": "2293672e396407a24caaa5788638e7a1e7fc962575bc2551f500fbc309c3fe32", "chronicle_feed.json": "60a80dd0042838b0412baf9dd0985c961b4253a6876d16bcdbb8a2c6c03de0c7", - "country_package.json": "5ab555a9ffa6f55598314b0886670c1446dde557c8e434eb6fd84857ec69a513", + "country_package.json": "a4f71abaf97cda60c0eedf329dcd3da47fe8167898a94184be2a9a88c55e5a7b", "currency_bridge.json": "aa6a4b286a6a1a015088f32ac013148553422c3d45eeef7d7a15af08388857ca", "export_contract.json": "2c7414fefe395c4f5822643dcd3bb3544ebb2040259aa007b828a99bd01ae210", "gates.json": "fa08b508ce1c93ef7627ca06f3a9e808f8fa6ea6f23ce24fa54c58241bbb9f7f", "geography_spine.json": "799bb85e0e4ef943b9f14c6223e2b6168d587e09d1fbf8c7ea2e1aaa0eaee17d", "holdout_references.json": "52b5a2360502156da1392a68350e4e7f61b5fd420fa74428819b65886a9f8655", "precal_references.json": "420361e424405df2312bd21808d6b6f4c3428b6fe90c44c37ab156ce197aa25d", - "reference_inventory.json": "a1356d3c7f91f4025a31a1edc68e2fe78017ec7a5eeb497d91031d4166e3765e", - "reference_profiles.json": "b0f23587f707c4af71eedfc450eae9175886f92e77d732a0e24758df83ec505d", + "reference_aging.json": "4987106611ce14b227e483016b384f6c8103e28311c60384df6adbdfba4ca75d", + "reference_inventory.json": "d16ac4e95f5427c7bb1382ff6ae90e93d4b48344d810755fcc1dc6514430244a", + "reference_profiles.json": "323896a354467a4ade5c52c97c2f80acbd62809072e652c801bd209caa667c54", "release_contract.json": "6cd5e3a4a2bf30e4025e04e964c393b24040937d13db7e6a07b3966a55f3b114", "reporting_classifiers.json": "2700a8dab0bcc186a0ce354a09c7cab127e8b07589b15fb7d63c7f1581041ad8", "scenarios.json": "3f1b5e40d283d7a1baf1a6990f2ca08a0ca39151a8cff028a49455f95e82e9c7", @@ -64,7 +65,7 @@ "spec/sources.yaml": "ee015a35820e4688c7325d24bf0dac0ad090226d15401d2e3b83bde4413561be", "spec/spine.yaml": "b3e6a2832ff7671777cae113b7fc45f5e3492bb519526f229118d0851083533e", "spec/vintages.yaml": "1ecc1a3f952bd25b834c394314e8c933ddb5b095fd64ba492dddb03e3cdd803e", - "target_references.json": "39c40fbd2277d68304b141dedd8e01800fd93f58d1950ad7cce8e103e4daeeca" + "target_references.json": "72b2d3b667c782b81b33cc850cd7f8b4014fc4d9fd50e74cb98c29dae1d46a46" }, "resources": [ "spec/bundle.yaml", @@ -86,6 +87,7 @@ "geography_spine.json", "holdout_references.json", "precal_references.json", + "reference_aging.json", "reference_inventory.json", "reference_profiles.json", "release_contract.json", diff --git a/test_support/microcosm_build/reference_factor_transport.py b/test_support/microcosm_build/reference_factor_transport.py new file mode 100644 index 0000000000..f3d539b864 --- /dev/null +++ b/test_support/microcosm_build/reference_factor_transport.py @@ -0,0 +1,169 @@ +"""Authenticated toy feed and companion references for transport factor checks.""" + +import json +from copy import deepcopy + +from microcosm.build.ledger_artifact import ( + chronicle_feed_identity, + load_ledger_consumer_artifact, +) +from microcosm.build.reference_factors import resolve_reference_factor +from microcosm.build.reference_profiles import ( + reference_document_sha256, + resolve_reference_profiles, +) +from test_support.microcosm_build.chronicle_feed import write_pinned_artifact +from test_support.microcosm_build.reference_factors import factor_fixture +from test_support.microcosm_build.transport_graph import reference_document + + +def factor_transport_fixture( + tmp_path, *, forged=False, activate=False, manifest_commit=False +): + """Build evidence from loaded lexical tokens; optionally forge its source claim.""" + _, facts, _ = factor_fixture() + references = [] + for kind, value in (("count", 3), ("money", 100)): + key = f"toy-{kind}" + selector = { + "aggregate_fact_key": key, + "source_name": "toy_office", + "source_measure_id": kind, + "source_concept": f"toy.{kind}", + "entity_name": "person", + "period_type": "year", + "period_value": 2001, + "dimensions": ["band"], + "dimension_values": {"band": "one"}, + } + facts.append( + { + "aggregate_fact_key": key, + "label": f"Toy published {kind}", + "value": value, + "assertion": "observation", + "period": {"type": "year", "value": 2001}, + "geography": { + "level": "country", + "id": "XX", + "name": "Toy country", + "vintage": "toy", + }, + "entity": {"name": "person"}, + "dimensions": {"band": "one"}, + "dimension_labels": {"band": "Band"}, + "dimension_value_labels": {"band": {"one": "One"}}, + "aggregation": {"method": "sum"}, + "source": {"source_name": "toy_office"}, + "observed_measure": { + "source_measure_id": kind, + "source_concept": f"toy.{kind}", + "unit": kind, + }, + } + ) + references.append( + { + "name": key, + "entity": "person", + "measure": "people" if kind == "count" else "employment_income", + "family": "toy", + "category_id": "toy.population", + "period": 2002, + "period_match_policy": "latest_not_after", + "ledger_selector": selector, + "metadata": { + "contract_target_id": key, + "reference_value_kind": kind, + "activation_status": "requires_prerequisites", + "required_reference_profile": "index", + "required_measurement_binding": "reviewed_measurement", + "reference_prerequisites": json.dumps( + ["reference_profile:index", "measurement:reviewed_measurement"] + ), + "parking_reason": "Reviewed measurement is required.", + }, + } + ) + path, pin = write_pinned_artifact( + tmp_path / "factor-artifact", json.loads(json.dumps(facts, default=float)) + ) + pin["source_commit"] = "c" * 40 + if manifest_commit: + import hashlib + + manifest_path = path / "manifest.json" + manifest = json.loads(manifest_path.read_text()) + manifest["source_commit"] = pin["source_commit"] + manifest_path.write_text(json.dumps(manifest, sort_keys=True) + "\n") + pin["manifest_sha256"] = hashlib.sha256(manifest_path.read_bytes()).hexdigest() + artifact = load_ledger_consumer_artifact(path, feed_pin=pin) + identity = chronicle_feed_identity(pin) + declaration, _, _ = factor_fixture(feed_identity=identity) + evidence_facts = artifact.facts + if forged: + # This evidence is internally consistent but falsely claims the original pin. + evidence_facts = deepcopy(artifact.facts) + next(row for row in evidence_facts if row["aggregate_fact_key"] == "target-0")[ + "value" + ] = 120 + declaration["expected_operational_factor"]["numerator"] = 1200000 + factor, receipt = resolve_reference_factor(declaration, evidence_facts, identity) + context = { + "schema_version": 1, + "feed_identity": identity, + "factors": [ + { + "declaration": declaration, + "operational_factor": factor, + "receipt": receipt, + } + ], + } + transformation = { + "operation": "scale_money_and_intervals", + "factor_id": declaration["id"], + "reference_names": ["toy-money"], + "count_reference_names": ["toy-count"], + "interval_table_ids": [], + "required_measurement_binding": "reviewed_measurement", + } + profiles = { + "schema_version": 1, + "reference_resource": "target_references", + "factor_context_resource": "factor_context", + "dimensions": { + "index": { + "default": "central", + "profiles": { + name: {"transformations": [deepcopy(transformation)]} + for name in ("low", "central", "high") + }, + } + }, + } + base = reference_document(references) + base["hierarchy"]["target_labels"] = { + row["name"]: f"Aged toy {row['name']}" for row in references + } + document, _, resolution = resolve_reference_profiles( + base, profiles, {}, factor_context=context + ) + if activate: + # Test-only future measurement closure lets the compiler test its own replay + # and arithmetic independently of this lane's parked country declarations. + for row in document["target_references"]: + row["metadata"]["activation_status"] = "active" + resolution["effective_references_sha256"] = reference_document_sha256(document) + resolution.pop("receipt_sha256") + resolution["receipt_sha256"] = reference_document_sha256(resolution) + spec = { + "country": "xx", + "resources": { + "target_references": base, + "reference_profiles": profiles, + "factor_context": context, + "scenarios": {"scenarios": [{"id": "base", "knobs": {}}]}, + }, + } + return path, pin, document, resolution, spec diff --git a/test_support/microcosm_build/reference_factors.py b/test_support/microcosm_build/reference_factors.py new file mode 100644 index 0000000000..ca962f70ab --- /dev/null +++ b/test_support/microcosm_build/reference_factors.py @@ -0,0 +1,185 @@ +"""Small engine-free declarations for exercising public factor interfaces.""" + +from __future__ import annotations + +from copy import deepcopy +from datetime import date, timedelta +from decimal import Decimal + +from microcosm.build.ledger_artifact import CONSUMER_ARTIFACT_SCHEMA_VERSION +from microcosm.build.reference_factors import resolve_reference_factor + + +def factor_fixture( + *, + feed_identity=None, + frequency=2, + source_level="100", + anchor_level="110", + growth="0", + digits=6, + expected_numerator=None, +): + """Return a declaration, exact numeric facts and a matching feed identity. + + Slots are single days: period coverage, rather than calendar conventions, + establishes contiguity. Callers may serialize facts into their toy artifact. + """ + identity = ( + { + "source_commit": "c" * 40, + "facts_sha256": "a" * 64, + "manifest_sha256": "b" * 64, + "artifact_schema_version": CONSUMER_ARTIFACT_SCHEMA_VERSION, + "consumer_fact_schema_versions": ["chronicle.consumer_fact.v4"], + "consumer_fact_schema_sha256": "d" * 64, + "fact_row_count": frequency + 2, + } + if feed_identity is None + else deepcopy(feed_identity) + ) + facts, source, target = [], [], [] + validation = { + "unit": "money_per_week", + "source_sha256": "a" * 64, + "source_vintage": "reviewed", + "entity_role": "employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + } + for schedule, start, prefix, level in ( + (source, date(2001, 1, 1), "source", source_level), + (target, date(2002, 1, 1), "target", anchor_level), + ): + for index in range(frequency): + period = (start + timedelta(days=index)).isoformat() + coverage = {"start_date": period, "end_date": period} + selector = { + "source_name": "toy_office", + "source_measure_id": "weekly_pay", + "entity_name": "person", + "period_type": "day", + "period_value": period, + "assertion": "observation", + "dimensions": [], + } + slot = { + "selector": selector, + "validation": {**validation, "period_coverage": coverage}, + "weight": "1", + "sequence": index, + } + if schedule is target: + slot["projection_offset"] = index + schedule.append(slot) + if schedule is source or index == 0: + facts.append( + { + "aggregate_fact_key": f"{prefix}-{index}", + "value": Decimal(level), + "assertion": "observation", + "provenance_class": "survey_aggregate", + "period": {"type": "day", "value": period}, + "period_coverage": coverage, + "entity": {"name": "person", "role": "employee"}, + "dimensions": {}, + "source": { + "source_name": "toy_office", + "source_sha256": "a" * 64, + "vintage": "reviewed", + }, + "observed_measure": { + "source_measure_id": "weekly_pay", + "unit": "money_per_week", + }, + } + ) + growth_period = "2002" + growth_validation = { + **validation, + "unit": "percent", + "assertion": "source_projection", + "provenance_class": "model_output", + "period_coverage": {"start_date": "2002-01-01", "end_date": "2002-12-31"}, + "source_column_id": "annual_projection", + } + facts.append( + { + "aggregate_fact_key": "growth", + "value": Decimal(growth), + "assertion": "source_projection", + "provenance_class": "model_output", + "period": {"type": "year", "value": growth_period}, + "period_coverage": growth_validation["period_coverage"], + "entity": {"name": "person", "role": "employee"}, + "dimensions": {}, + "source": { + "source_name": "toy_office", + "source_sha256": "a" * 64, + "vintage": "reviewed", + }, + "observed_measure": {"source_measure_id": "growth", "unit": "percent"}, + "layout": {"source_column_id": "annual_projection"}, + } + ) + if expected_numerator is None: + expected_numerator = int( + Decimal(anchor_level) / Decimal(source_level) * 10**digits + ) + declaration = { + "schema_version": 1, + "id": "toy_factor", + "operation": "projected_period_average_ratio", + "operation_version": 1, + "purpose": "profile_input", + "feed_identity": identity, + "source_period": 2001, + "target_period": 2002, + "source_schedule": source, + "target_schedule": target, + "growth": { + "selector": {"aggregate_fact_key": "growth", "period_value": growth_period}, + "validation": growth_validation, + "unit_multiplier": "0.01", + "scenario_delta": "0", + "sensitivity_provenance": ["toy reviewed sensitivity"], + "assertion_policy": "allow_source_projection", + }, + "subperiods_per_year": frequency, + "projection_policy": "observed_preferred_latest_contiguous", + "rounding": {"digits": digits, "mode": "nearest_half_even"}, + "expected_operational_factor": { + "numerator": expected_numerator, + "denominator": 10**digits, + }, + "validity": { + "declared_on": "2000-01-01", + "valid_from": "2000-01-01", + "valid_until": "2099-12-31", + "as_of": "2002-01-01", + }, + "evidence": ["toy published levels"], + "decision_references": ["toy reviewed decision"], + } + return declaration, facts, identity + + +def factor_context_fixture(**kwargs): + """Return a context with a factor genuinely resolved through the public API.""" + declaration, facts, identity = factor_fixture(**kwargs) + factor, receipt = resolve_reference_factor(declaration, facts, identity) + return ( + { + "schema_version": 1, + "feed_identity": identity, + "factors": [ + { + "declaration": declaration, + "operational_factor": factor, + "receipt": receipt, + } + ], + }, + facts, + identity, + ) diff --git a/tools/generate_nz_reference_aging.py b/tools/generate_nz_reference_aging.py new file mode 100644 index 0000000000..e40961b972 --- /dev/null +++ b/tools/generate_nz_reference_aging.py @@ -0,0 +1,241 @@ +"""Generate NZ's reviewed ageing declarations and authenticated factor receipts. + +Run with ``NZ_CHRONICLE_ARTIFACT`` or ``--artifact``. The six operational +expectations are assertions of the feed calculation, never arithmetic inputs. +The observed-only ratio is diagnostic and is not available to profiles. +""" + +from __future__ import annotations + +import argparse +import copy +import json +import os +from pathlib import Path + +from microcosm.build.ledger_artifact import ( + chronicle_feed_identity, + load_ledger_consumer_artifact, + validate_chronicle_feed_pin, +) +from microcosm.build.reference_factors import resolve_reference_factor +from tools.generate_nz_target_references import SPEC, _selector + +_QES_2025_HASH = "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30" +_QES_2026_HASH = "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97" +_TREASURY_HASH = "e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9" +_OPERANDS = ( + (12028, "2023-Q2", "cfbce1343aa646fce0411631", "1502.27"), + (12030, "2023-Q3", "f9f4f127d080fe2f06617947", "1528.92"), + (12032, "2023-Q4", "fb5938810b7b37ae8534e5f7", "1556.16"), + (12034, "2024-Q1", "854e91ec1dd95e396659ad8e", "1563.54"), + (12044, "2025-Q2", "86ce22ba42a6e2a36b889f9b", "1652.72"), + (12046, "2025-Q3", "434e7b2736b4b16589034c72", "1661.12"), + (12048, "2025-Q4", "bc87e53157eaf53c846879dc", "1681.06"), + (12050, "2026-Q1", "78e735569814069f8066ba9f", "1685.39"), + (12052, "2026-Q2", "91583e3f39d11b8b4b2923d2", "1700.68"), +) +_TY2024 = ("2023-Q2", "2023-Q3", "2023-Q4", "2024-Q1") +_TY2026 = ("2025-Q2", "2025-Q3", "2025-Q4", "2026-Q1") +_TARGET = ("2026-Q2", "2026-Q3", "2026-Q4", "2027-Q1") +_EXPECTED = { + "ird_ty2024_to_ty2027_low": 1115056, + "ird_ty2024_to_ty2027_central": 1117109, + "ird_ty2024_to_ty2027_high": 1119158, + "p1_ty2026_to_ty2027_low": 1026690, + "p1_ty2026_to_ty2027_central": 1028580, + "p1_ty2026_to_ty2027_high": 1030467, + "observed_ty2026_over_ty2024": 1086069, +} +_DECISIONS = [ + "d1599; IRD band ageing factors, 22 October 2026", + "G8-r2 hub rulings 3, 23 October 2026, sections 1–2", +] + + +def _coverage(period): + year, quarter = period.split("-Q") + year, quarter = int(year), int(quarter) + start_month = 3 * quarter - 2 + end_day = (31, 30, 30, 31)[quarter - 1] + return { + "start_date": f"{year:04d}-{start_month:02d}-01", + "end_date": f"{year:04d}-{start_month + 2:02d}-{end_day}", + } + + +def _validation(fact): + return { + "unit": fact["observed_measure"]["unit"], + "source_sha256": fact["source"]["source_sha256"], + "source_vintage": fact["source"]["vintage"], + "entity_role": fact["entity"]["role"], + "provenance_class": fact["provenance_class"], + "assertion": fact["assertion"], + "period_coverage": { + key: fact["period_coverage"][key] for key in ("start_date", "end_date") + }, + "source_column_id": fact["layout"]["source_column_id"], + } + + +def build_aging_declarations(artifact, *, package_dir=SPEC): + """Author the reviewed fixed selectors, refusing a changed operand or pin.""" + feed_pin = json.loads((package_dir / "chronicle_feed.json").read_text()) + validate_chronicle_feed_pin(artifact, feed_pin) + feed_pin = chronicle_feed_identity(feed_pin) + facts_by_key = {fact["aggregate_fact_key"]: fact for fact in artifact.facts} + qes = {} + for _line, period, suffix, expected in _OPERANDS: + fact = facts_by_key[f"ledger.aggregate_fact.v2:{suffix}"] + if str(fact["value"]) != expected or fact["period"]["value"] != period: + raise ValueError(f"Reviewed QES operand changed for {period}") + historical = period in _TY2024 + expected_hash = _QES_2025_HASH if historical else _QES_2026_HASH + if ( + fact["source"]["source_sha256"] != expected_hash + or fact["observed_measure"]["source_measure_id"] != "SBSZ9A" + or fact["observed_measure"]["unit"] != "nzd_per_week" + or fact["assertion"] != "observation" + ): + raise ValueError(f"Reviewed QES evidence changed for {period}") + qes[period] = fact + treasury = facts_by_key["ledger.aggregate_fact.v2:aba7d3d54489b068116e6a25"] + if ( + str(treasury["value"]) != "2.7" + or treasury["period"]["value"] != 2026 + or treasury["source"]["source_sha256"] != _TREASURY_HASH + or treasury["layout"]["source_column_id"] != "year_ending_30_june_2027" + ): + raise ValueError("Reviewed PREFU operand changed") + + def schedule(periods, *, target=False): + result = [] + for index, period in enumerate(periods): + fact = qes.get(period, qes["2026-Q2"]) + selector = _selector(fact, pin_key=period in qes) + selector["assertion"] = "observation" + selector["period_value"] = period + selector["record_set_id"] = ( + f"stats_nz.qes.awe.{period.lower().replace('-', '')}" + ) + validation = _validation(fact) + validation["period_coverage"] = _coverage(period) + slot = { + "selector": selector, + "validation": validation, + "weight": "1", + "sequence": index, + } + if target: + slot["projection_offset"] = index + result.append(slot) + return result + + growth_selector = _selector(treasury) + growth_selector["assertion"] = "source_projection" + growth_selector["source_table"] = treasury["observed_measure"]["source_table"] + declarations = [] + for prefix, periods, source_period in ( + ("ird_ty2024", _TY2024, 2024), + ("p1_ty2026", _TY2026, 2026), + ): + for profile, delta in (("low", "-0.5"), ("central", "0"), ("high", "0.5")): + factor_id = f"{prefix}_to_ty2027_{profile}" + declarations.append( + { + "schema_version": 1, + "id": factor_id, + "operation": "projected_period_average_ratio", + "operation_version": 1, + "purpose": "profile_input", + "feed_identity": copy.deepcopy(feed_pin), + "source_period": source_period, + "target_period": 2027, + "source_schedule": schedule(periods), + "target_schedule": schedule(_TARGET, target=True), + "growth": { + "selector": copy.deepcopy(growth_selector), + "validation": _validation(treasury), + "unit_multiplier": "0.01", + "scenario_delta": delta, + "assertion_policy": "allow_source_projection", + "sensitivity_provenance": [ + "d1599: symmetric ±0.5 percentage-point Microcosm sensitivity; high is not a Treasury forecast path" + ], + }, + "subperiods_per_year": 4, + "projection_policy": "observed_preferred_latest_contiguous", + "rounding": {"digits": 6, "mode": "nearest_half_even"}, + "expected_operational_factor": { + "numerator": _EXPECTED[factor_id], + "denominator": 1000000, + }, + "validity": { + "declared_on": "2026-10-23", + "valid_from": "2026-10-23", + "valid_until": "2026-11-03", + "as_of": "2026-10-23", + }, + "evidence": [ + "Stats NZ QES Table 7; QEMQ.SBSZ9A; ordinary-time FTE, all sectors, actual", + "Treasury PREFU 2026 Table 4.1; year ending 30 June 2027; Chronicle period 2026", + ], + "decision_references": list(_DECISIONS), + } + ) + diagnostic = copy.deepcopy(declarations[1]) + diagnostic.update( + { + "id": "observed_ty2026_over_ty2024", + "purpose": "diagnostic", + "target_period": 2026, + "target_schedule": schedule(_TY2026, target=True), + "growth": None, + "expected_operational_factor": { + "numerator": _EXPECTED["observed_ty2026_over_ty2024"], + "denominator": 1000000, + }, + } + ) + declarations.append(diagnostic) + return feed_pin, declarations + + +def build_reference_aging(artifact, *, package_dir=SPEC): + feed_pin, declarations = build_aging_declarations(artifact, package_dir=package_dir) + records = [] + for declaration in declarations: + factor, receipt = resolve_reference_factor( + declaration, artifact.facts, feed_pin + ) + records.append( + { + "declaration": declaration, + "operational_factor": factor, + "receipt": receipt, + } + ) + return {"schema_version": 1, "feed_identity": feed_pin, "factors": records} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--artifact", default=os.environ.get("NZ_CHRONICLE_ARTIFACT")) + parser.add_argument("--out", type=Path, default=SPEC / "reference_aging.json") + parser.add_argument("--check", action="store_true") + args = parser.parse_args() + if not args.artifact: + parser.error("--artifact or NZ_CHRONICLE_ARTIFACT is required") + artifact = load_ledger_consumer_artifact(args.artifact) + document = build_reference_aging(artifact) + encoded = json.dumps(document, ensure_ascii=False, indent=2) + "\n" + if args.check: + if args.out.read_text() != encoded: + raise SystemExit(f"Stale generated resource: {args.out}") + else: + args.out.write_text(encoded) + + +if __name__ == "__main__": + main() diff --git a/tools/generate_nz_target_references.py b/tools/generate_nz_target_references.py index 1a3f309a81..af68d92fc8 100644 --- a/tools/generate_nz_target_references.py +++ b/tools/generate_nz_target_references.py @@ -79,7 +79,11 @@ "disclosed national-growth proxy." ) FAMILY_REASON = "Stats NZ Census-family nucleus standard and deterministic roster producer are not encoded; MC7 benefit units and family labels cannot substitute for Census multiplicities." -IRD_REASON = "G8-r2: reviewed IRD-definition taxable-income binding, component closure, record universe, numerical intervals and B5 ageing remain required." +IRD_REASON = "B4: reviewed IRD-definition taxable-income measurement, annual component closure and record universe remain required. Ageing resolution does not establish that measurement." +IRD_MEASUREMENT_BINDING = "ird_taxable_income_ty2027_v1" +# Reviewed national detail-row map: nil row 8 through open tail row 244. +# Published national total row 246 is diagnostic and is deliberately absent. +IRD_NATIONAL_ROWS = tuple(f"row_{row}" for row in range(8, 245)) RECEIPT_REASON = "G8-w: age-stratified receipt assignment is not supported by the current total-only receipt kernel; preserve the source snapshot and age filters." NZS_REASON = "NZ Super receipt binding and prepared receipt flag are not implemented." VP_REASON = "Veterans Pension is unsupported: no veteran facts or producer; a missing flag must refuse, never default to false." @@ -537,8 +541,10 @@ def build_reference_documents(artifact, *, package_dir=SPEC): (f for f in facts if f["layout"]["table_record_kind"] == "detail"), key=lambda f: int(f["layout"]["source_row_id"].removeprefix("row_")), ) - if len(details) != 237: - raise ValueError("Expected 237 taxable-income detail bands") + if tuple(f["layout"]["source_row_id"] for f in details) != IRD_NATIONAL_ROWS: + raise ValueError( + "Expected the reviewed 237 taxable-income detail rows 8–244" + ) for fact in details: row_id = fact["layout"]["source_row_id"] targets.append( @@ -550,12 +556,26 @@ def build_reference_documents(artifact, *, package_dir=SPEC): measure=column, family="income", period=2027, - status="requires_g8r2_taxable_income_contract_intervals_and_ageing", + status="requires_prerequisites", reason=IRD_REASON, filter_column=f"taxable_income_band_{row_id}_indicator", label=f"TY2026-27 {contract.replace('_', ' ')} {fact['dimensions']['taxable_income_band_source_label']} (parked)", ) ) + targets[-1]["metadata"].update( + { + "reference_prerequisites": json.dumps( + [ + "reference_profile:aging_index", + f"measurement:{IRD_MEASUREMENT_BINDING}", + ], + separators=(",", ":"), + ), + "required_reference_profile": "aging_index", + "required_measurement_binding": IRD_MEASUREMENT_BINDING, + "reference_value_kind": "count" if column == "people" else "money", + } + ) wage_facts = inventory.cells( "ird_wage_salary_distribution.ty2026.income_bands", measure="individuals" ) @@ -705,6 +725,7 @@ def build_reference_documents(artifact, *, package_dir=SPEC): "identity", "rolled_forward_by_ratio", "allocated_residual_by_ratio", + "linear_combination", ] if reference_set == "calibration" else ["identity"], @@ -791,14 +812,31 @@ def _reference_profiles(inventory, rows): profiles[profile] = {"transformations": transformations} return { "schema_version": 1, + "factor_context_resource": "reference_aging.json", "dimensions": { "census_residual_allocation": {"default": "mar", "profiles": profiles}, "aging_index": { "default": "central", "profiles": { - "central": {"transformations": [], "deferred": True}, - "low": {"available": False, "transformations": []}, - "high": {"available": False, "transformations": []}, + profile: { + "transformations": [ + { + "operation": "scale_money_and_intervals", + "factor_id": f"ird_ty2024_to_ty2027_{profile}", + "reference_names": [ + f"ird_taxable_income_total_by_band__{row_id}" + for row_id in IRD_NATIONAL_ROWS + ], + "count_reference_names": [ + f"ird_taxable_income_people_by_band__{row_id}" + for row_id in IRD_NATIONAL_ROWS + ], + "interval_table_ids": [], + "required_measurement_binding": IRD_MEASUREMENT_BINDING, + } + ] + } + for profile in ("low", "central", "high") }, }, }, @@ -1017,7 +1055,7 @@ def _receipts(inventory, erp, documents, feed_pin, ta_facts): ), ( "qes_prefu_and_b5_ageing", - "G8-r2; QES/PREFU rows are present in the pinned artifact but unused by this residual-allocation task.", + "QES/PREFU feed rows support the declared factor calculations in reference_aging.json; they never become direct calibration references or carried target values.", ), ( "wff_tripwires", From 1d5fff46eb3fce592a893f1cf2f73774e6bdcd6b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 24 Oct 2026 04:52:42 -0400 Subject: [PATCH 02/24] Bind factor application to roster roles; make lexical floats opt-in Fixes the #1382 review (Sol r1) and the UK hub's loader notes. - Factor application: each transformed row's role (money or count) now comes from the selected profile's companion roster, not the rehashable action label. Money rows must carry exactly the verified singleton linear_combination; count rows keep their original operation, operands and kind. Both reviewer tampering cases refuse at the resolver and at compilation. - Lexical floats: load_ledger_consumer_artifact(lexical_floats=False) by default returns builtin floats again. Only compile replay with selected factors and the NZ ageing generator opt in; a static test pins those call sites. - _strict_scalar_equal treats LexicalFloat as float; bool, int and float stay distinct. - Oracle: exact roots return a zero-width enclosure and ambiguous bounds are refined; the (32, 1, 0, 2, 4) tie is an explicit example. - RuleSpec scan: only the seven Chronicle identity fields are exempt. - Factor schedules: frequency 1-12 and digits 0-18, so exact receipts stay serializable. Tests: 24 engine-free files, 3,024 passed and 5 skipped (UK feed not configured), one file per run; six new mutants killed. Co-Authored-By: GPT-6.1 Sol Co-Authored-By: Claude Opus 5.5 --- .../nz-reference-ageing-factors.added.md | 12 +- .../src/microcosm/build/ledger_artifact.py | 24 ++-- .../src/microcosm/build/ledger_targets.py | 5 +- .../src/microcosm/build/reference_factors.py | 35 ++++-- .../src/microcosm/build/reference_profiles.py | 92 ++++++++++++++- .../build/transport/target_kernels.py | 7 +- .../shared/test_ledger_artifact.py | 87 +++++++++++++- .../engine_free/shared/test_ledger_targets.py | 57 +++++++++ .../shared/test_nz_reference_aging.py | 2 +- .../shared/test_nz_spec_package.py | 35 ++++-- .../shared/test_reference_factors.py | 102 +++++++++++++--- .../shared/test_reference_profiles.py | 111 ++++++++++++++++++ .../shared/test_transport_target_kernels.py | 75 ++++++++++++ .../reference_factor_transport.py | 2 +- tools/generate_nz_reference_aging.py | 2 +- 15 files changed, 587 insertions(+), 61 deletions(-) diff --git a/changelog.d/nz-reference-ageing-factors.added.md b/changelog.d/nz-reference-ageing-factors.added.md index d625f48eb6..2f8a2967d2 100644 --- a/changelog.d/nz-reference-ageing-factors.added.md +++ b/changelog.d/nz-reference-ageing-factors.added.md @@ -4,6 +4,12 @@ compiler replay. Declare the reviewed NZ ageing profiles and their explicit mone and count companions while keeping the income references parked for measurement closure. -Keep authenticated decimal tokens in compact slotted floats that survive copying -and pickling. Bind factor evidence to reviewed feed identities while allowing -prose-only pin edits; refuse replay when the source commit cannot be authenticated. +Retain authenticated decimal tokens only with explicit lexical-float loading for +selected factors; ordinary loads keep plain floats and their serializer and memory +behavior. Bind factor application to the selected money/count roster and preserve +count operations. Certify exact-root ties in the independent oracle and bound +supported schedules so exact receipts remain serializable. Float selector pins +match token-retaining floats while keeping booleans, integers and floats distinct. +Scan extra feed-identity fields for RuleSpec commits. Bind factor evidence to +reviewed feed identities while allowing prose-only pin edits; refuse replay when +the source commit cannot be authenticated. diff --git a/packages/microcosm-build/src/microcosm/build/ledger_artifact.py b/packages/microcosm-build/src/microcosm/build/ledger_artifact.py index 8d35f4b2ff..c181d68e9a 100644 --- a/packages/microcosm-build/src/microcosm/build/ledger_artifact.py +++ b/packages/microcosm-build/src/microcosm/build/ledger_artifact.py @@ -108,11 +108,12 @@ def chronicle_feed_identity(feed_pin: Mapping[str, Any]) -> dict[str, Any]: class LexicalFloat(float): - """An ordinary float retaining its authenticated JSON numeric token. + """An opt-in float retaining its authenticated JSON numeric token. - Existing consumers and JSON serialization retain their float behavior. - Exact-arithmetic consumers can inspect ``lexical_token`` and prove that - converting the float back to decimal reproduces the published operand. + Request ``lexical_floats=True`` when loading operands for exact arithmetic. + The default loader returns plain floats, avoiding the instance and token + overhead and preserving compatibility with serializers requiring exact + float types. Standard JSON serialization retains the same numeric bytes. """ __slots__ = ("lexical_token",) @@ -296,6 +297,7 @@ def load_ledger_consumer_artifact( expected_facts_sha256: str | None = None, expected_manifest_sha256: str | None = None, feed_pin: Mapping[str, Any] | None = None, + lexical_floats: bool = False, ) -> LedgerConsumerArtifact: """Load a Ledger consumer artifact directory or bare consumer-facts file. @@ -305,6 +307,8 @@ def load_ledger_consumer_artifact( reviewed against; any mismatch raises before facts are used. ``feed_pin`` additionally checks all six reviewed artifact fields, without replacing the manifest integrity checks or either optional hash check. + ``lexical_floats=True`` retains numeric tokens for factor verification; + otherwise JSON decimals load as plain ``float`` instances. """ artifact_path = Path(path) if not artifact_path.exists(): @@ -360,7 +364,7 @@ def load_ledger_consumer_artifact( if feed_pin is not None: artifact = LedgerConsumerArtifact( path=artifact_path, - facts=_load_fact_rows(facts_path), + facts=_load_fact_rows(facts_path, lexical_floats=lexical_floats), facts_sha256=facts_sha256, manifest=manifest, manifest_sha256=manifest_sha256, @@ -386,7 +390,7 @@ def load_ledger_consumer_artifact( if artifact is None: artifact = LedgerConsumerArtifact( path=artifact_path, - facts=_load_fact_rows(facts_path), + facts=_load_fact_rows(facts_path, lexical_floats=lexical_floats), facts_sha256=facts_sha256, manifest=manifest, manifest_sha256=manifest_sha256, @@ -435,7 +439,9 @@ def resolve_ledger_artifact(args: Namespace) -> LedgerConsumerArtifact | None: ) -def _load_fact_rows(path: Path) -> tuple[dict[str, Any], ...]: +def _load_fact_rows( + path: Path, *, lexical_floats: bool = False +) -> tuple[dict[str, Any], ...]: rows: list[dict[str, Any]] = [] with path.open() as file: for line_number, line in enumerate(file, start=1): @@ -443,7 +449,9 @@ def _load_fact_rows(path: Path) -> tuple[dict[str, Any], ...]: if not stripped: continue try: - row = json.loads(stripped, parse_float=LexicalFloat) + row = json.loads( + stripped, parse_float=LexicalFloat if lexical_floats else float + ) except json.JSONDecodeError as exc: raise ValueError( f"Invalid Ledger facts JSONL row {line_number}: {exc.msg}" diff --git a/packages/microcosm-build/src/microcosm/build/ledger_targets.py b/packages/microcosm-build/src/microcosm/build/ledger_targets.py index 3af241ca1c..035f18ad8a 100644 --- a/packages/microcosm-build/src/microcosm/build/ledger_targets.py +++ b/packages/microcosm-build/src/microcosm/build/ledger_targets.py @@ -20,6 +20,7 @@ from types import MappingProxyType from typing import Any +from microcosm.build.ledger_artifact import LexicalFloat from microcosm.calibrate import ( CalibrationHierarchy, CalibrationHierarchySeed, @@ -5058,7 +5059,9 @@ def _dimension_values_match(fact: object, expected: object) -> bool: def _strict_scalar_equal(actual: object, expected: object) -> bool: - return type(actual) is type(expected) and actual == expected + actual_type = float if type(actual) is LexicalFloat else type(actual) + expected_type = float if type(expected) is LexicalFloat else type(expected) + return actual_type is expected_type and actual == expected def _measure_concepts(fact: object) -> tuple[str, ...]: diff --git a/packages/microcosm-build/src/microcosm/build/reference_factors.py b/packages/microcosm-build/src/microcosm/build/reference_factors.py index b8ad41911f..387c64b958 100644 --- a/packages/microcosm-build/src/microcosm/build/reference_factors.py +++ b/packages/microcosm-build/src/microcosm/build/reference_factors.py @@ -24,6 +24,12 @@ OPERATION = "projected_period_average_ratio" OPERATION_VERSION = 1 +# Exact powers in projected receipts grow with both schedule size and root +# precision. Keep ordinary schedules within the stock integer serialization +# budget, including when rounding needs a finer root enclosure. +_MAX_SUBPERIODS_PER_YEAR = 12 +_MAX_ROUNDING_DIGITS = 18 +_MAX_RECEIPT_ROOT_PLACES = 256 _DECLARATION_FIELDS = frozenset( { "schema_version", @@ -186,11 +192,20 @@ def _declaration(declaration: object) -> dict: if row["projection_policy"] != "observed_preferred_latest_contiguous": raise ValueError("Unknown factor projection policy.") frequency = row["subperiods_per_year"] - if type(frequency) is not int or not 1 <= frequency <= 366: - raise ValueError("subperiods_per_year must be a declared positive integer.") + if type(frequency) is not int or not 1 <= frequency <= _MAX_SUBPERIODS_PER_YEAR: + raise ValueError( + "subperiods_per_year must be an integer from 1 to " + f"{_MAX_SUBPERIODS_PER_YEAR} for serializable exact receipts." + ) rounding = _closed(row["rounding"], {"digits", "mode"}, "factor rounding") - if type(rounding.get("digits")) is not int or not 0 <= rounding["digits"] <= 100: - raise ValueError("Factor rounding digits must be an integer from 0 to 100.") + if ( + type(rounding.get("digits")) is not int + or not 0 <= rounding["digits"] <= _MAX_ROUNDING_DIGITS + ): + raise ValueError( + "Factor rounding digits must be an integer from 0 to " + f"{_MAX_ROUNDING_DIGITS} for serializable exact receipts." + ) if rounding.get("mode") != "nearest_half_even": raise ValueError("Factor rounding must be nearest_half_even.") expected = _read_rational(row["expected_operational_factor"], "expected factor") @@ -403,7 +418,10 @@ def _operand( "decimal_values_equal": True, } else: - raise ValueError("Factor operand lacks an authenticated lexical numeric token.") + raise ValueError( + "Factor operand lacks an authenticated lexical numeric token; " + "load the artifact with lexical_floats=True." + ) numeric = _decimal(token, "feed numeric token") return numeric, { "selector": selector, @@ -539,7 +557,7 @@ def numerator_at(root: Fraction) -> Fraction: digits = row["rounding"]["digits"] precision = digits + 8 - while precision <= 4096: + while precision <= _MAX_RECEIPT_ROOT_PLACES: lower, upper, exact = certified_nth_root_bounds( 1 + effective_growth, frequency, precision ) @@ -556,7 +574,10 @@ def numerator_at(root: Fraction) -> Fraction: break precision *= 2 else: - raise ValueError("Factor rounding cannot be certified unambiguously.") + raise ValueError( + "Factor rounding cannot be certified within the supported exact receipt " + "precision." + ) factor = {"numerator": rounded_lower, "denominator": 10**digits} if rounded_lower <= 0: raise ValueError("Operational factor must be positive.") diff --git a/packages/microcosm-build/src/microcosm/build/reference_profiles.py b/packages/microcosm-build/src/microcosm/build/reference_profiles.py index 881fdbf496..9c1fdebf00 100644 --- a/packages/microcosm-build/src/microcosm/build/reference_profiles.py +++ b/packages/microcosm-build/src/microcosm/build/reference_profiles.py @@ -312,6 +312,8 @@ def resolve_reference_profiles( if selected_factors: receipt.update( resolver_version=FACTOR_REFERENCE_PROFILE_RESOLVER_VERSION, + selected_declarations=_copy(chosen), + base_reference_document=_copy(base), selected_factors=_copy(selected_factors), selected_factor_identities={ factor_id: { @@ -511,6 +513,45 @@ def resolve_reference_profiles( return effective, intervals, receipt +def _factor_reference_bindings(resolution: Mapping) -> tuple[dict, set]: + """Recover each factor row's role and source behavior from bound documents.""" + if resolution["resolver_version"] == REFERENCE_PROFILE_RESOLVER_VERSION: + return {}, set() + chosen = _mapping(resolution.get("selected_declarations"), "selected declarations") + base = _mapping(resolution.get("base_reference_document"), "base references") + if reference_document_sha256(chosen) != resolution.get( + "selected_declarations_sha256" + ) or reference_document_sha256(base) != resolution.get("base_references_sha256"): + raise ValueError("Factor companion source document identity mismatch.") + if { + dimension: choice.get("name") for dimension, choice in chosen.items() + } != resolution.get("selection"): + raise ValueError("Selected profile declaration identity mismatch.") + source_rows = _sequence(base.get("target_references"), "base target references") + by_name = {row["name"]: row for row in source_rows} + if len(by_name) != len(source_rows): + raise ValueError("Base reference names must be unique.") + bindings, other_names = {}, set() + for dimension, choice in chosen.items(): + for transform in choice["declaration"].get("transformations", []): + if transform.get("operation") != SCALE_MONEY_AND_INTERVALS: + other_names.update(_explicit_names(transform, "reference_names")) + continue + money, counts = _validate_companions(transform, by_name) + for role, names in (("money", money), ("count", counts)): + for name in names: + if name in bindings: + raise ValueError("Duplicate factor companion roster entry.") + bindings[name] = { + "dimension": dimension, + "profile": choice["name"], + "factor_id": transform["factor_id"], + "role": role, + "source": by_name[name], + } + return bindings, other_names + + def validate_reference_resolution( document: Mapping, receipt: Mapping, @@ -545,6 +586,7 @@ def validate_reference_resolution( interval_document ) != resolution.get("effective_intervals_sha256"): raise ValueError("Effective interval document hash mismatch.") + factor_bindings, other_names = _factor_reference_bindings(resolution) names, used_factors = [], set() for row in document.get("target_references", ()): metadata = row.get("metadata", {}) @@ -556,8 +598,35 @@ def validate_reference_resolution( ): raise ValueError("Effective reference profile identity mismatch.") action = resolution.get("transformations", {}).get(row["name"], {}) - if "factor_id" in action: - factor_id = action["factor_id"] + binding = factor_bindings.get(row["name"]) + if factors and binding is None and row["name"] not in other_names: + raise ValueError( + "Factor action names a row outside its companion roster." + ) + if binding is not None or "factor_id" in action: + if binding is None: + raise ValueError( + "Factor action names a row outside its companion roster." + ) + role = binding["role"] + operation = "linear_combination" if role == "money" else "identity" + if action.get("operation") != operation or any( + action.get(key) != binding[key] + for key in ("dimension", "profile", "factor_id") + ): + raise ValueError( + "Factor action disagrees with its companion roster role." + ) + source = binding["source"] + if ( + metadata.get("reference_value_kind") != role + or row.get("ledger_selector") != source.get("ledger_selector") + or row.get("period") != source.get("period") + ): + raise ValueError( + "Effective factor companion source semantics changed." + ) + factor_id = binding["factor_id"] if factor_id not in factors: raise ValueError( "A transformed reference has missing factor evidence." @@ -572,10 +641,10 @@ def validate_reference_resolution( or action.get("target_period") != declaration["target_period"] ): raise ValueError("Effective reference factor identity mismatch.") - if action.get("operation") == "linear_combination": + if role == "money": expected = [ { - "aggregate_fact_key": row.get("ledger_selector", {}).get( + "aggregate_fact_key": source.get("ledger_selector", {}).get( "aggregate_fact_key" ), "expected_member_count": 1, @@ -585,14 +654,25 @@ def validate_reference_resolution( if ( row.get("value_operation") != "linear_combination" or row.get("value_operands") != expected + or action.get("operand_selectors") != expected ): raise ValueError( "Effective money factor must be applied exactly once." ) - elif action.get("operation") != "identity": - raise ValueError("Unsupported factor transformation action.") + elif ( + row.get("value_operation", "identity") + != source.get("value_operation", "identity") + or row.get("value_operands", []) != source.get("value_operands", []) + or action.get("operand_selectors") + != source.get("value_operands", []) + ): + raise ValueError( + "Effective count value semantics must remain unchanged." + ) if sorted(names) != resolution.get("transformed_names"): raise ValueError("Resolution receipt transformed names mismatch.") if used_factors != set(factors): raise ValueError("Resolution receipt has unused selected factor evidence.") + if set(factor_bindings) - set(names): + raise ValueError("Resolution receipt is missing factor companion rows.") return resolution diff --git a/packages/microcosm-build/src/microcosm/build/transport/target_kernels.py b/packages/microcosm-build/src/microcosm/build/transport/target_kernels.py index 8e3c14221a..efd5fbe291 100644 --- a/packages/microcosm-build/src/microcosm/build/transport/target_kernels.py +++ b/packages/microcosm-build/src/microcosm/build/transport/target_kernels.py @@ -399,7 +399,12 @@ def compile_target_surface( document, reference_resolution, references_sha256=references_sha256 ) artifact = load_ledger_consumer_artifact( - facts_path, expected_facts_sha256=expected_facts_sha256, feed_pin=feed_pin + facts_path, + expected_facts_sha256=expected_facts_sha256, + feed_pin=feed_pin, + lexical_floats=bool( + reference_resolution and reference_resolution.get("selected_factors") + ), ) if reference_resolution is not None: for entry in reference_resolution.get("selected_factors", {}).values(): diff --git a/packages/microcosm-build/tests/engine_free/shared/test_ledger_artifact.py b/packages/microcosm-build/tests/engine_free/shared/test_ledger_artifact.py index 557e6216da..7dc217711d 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_ledger_artifact.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_ledger_artifact.py @@ -2,6 +2,7 @@ from __future__ import annotations +import ast import hashlib import json import os @@ -26,6 +27,7 @@ resolve_ledger_artifact, validate_chronicle_feed_pin, ) +from microcosm.graph.canonical import canonical_json from test_support.microcosm_build.chronicle_feed import write_pinned_artifact from test_support.paths import paths_for @@ -96,7 +98,7 @@ def test_authenticated_float_retains_lexical_token_without_changing_json(tmp_pat facts_path.write_text(original) facts_sha = hashlib.sha256(original.encode()).hexdigest() artifact = load_ledger_consumer_artifact( - facts_path, expected_facts_sha256=facts_sha + facts_path, expected_facts_sha256=facts_sha, lexical_floats=True ) value = artifact.facts[0]["value"] assert isinstance(value, LexicalFloat) @@ -111,11 +113,53 @@ def test_authenticated_float_retains_lexical_token_without_changing_json(tmp_pat assert artifact.facts_sha256 == facts_sha +@pytest.mark.parametrize("pinned", [False, True]) +def test_default_float_loading_returns_exact_builtin_float(tmp_path, pinned): + path, pin = write_pinned_artifact(tmp_path / "artifact", [_fact_row(value=0.1)]) + artifact = load_ledger_consumer_artifact( + path, **({"feed_pin": pin} if pinned else {}) + ) + + assert type(artifact.facts[0]["value"]) is float + + +def test_lexical_float_opt_in_is_limited_to_factor_consumers(): + repository = paths_for("microcosm-build").repository + sources = list((repository / "packages").glob("*/src/**/*.py")) + sources.extend((repository / "tools").rglob("*.py")) + opt_ins = set() + for path in sources: + for node in ast.walk(ast.parse(path.read_text())): + if not isinstance(node, ast.Call): + continue + for keyword in node.keywords: + if keyword.arg != "lexical_floats": + continue + # A loader forwarding its caller's choice is not an opt-in. + if ( + isinstance(keyword.value, ast.Name) + and keyword.value.id == "lexical_floats" + ): + continue + if ( + isinstance(keyword.value, ast.Constant) + and keyword.value.value is False + ): + continue + opt_ins.add(path.relative_to(repository).as_posix()) + assert opt_ins == { + "packages/microcosm-build/src/microcosm/build/transport/target_kernels.py", + "tools/generate_nz_reference_aging.py", + } + + @pytest.mark.parametrize("protocol", range(pickle.HIGHEST_PROTOCOL + 1)) def test_authenticated_float_pickle_preserves_lexical_token(tmp_path, protocol): facts_path = tmp_path / "consumer_facts.jsonl" facts_path.write_text('{"aggregate_fact_key":"decimal","value":1.2500e2}\n') - value = load_ledger_consumer_artifact(facts_path).facts[0]["value"] + value = load_ledger_consumer_artifact(facts_path, lexical_floats=True).facts[0][ + "value" + ] restored = pickle.loads(pickle.dumps(value, protocol=protocol)) @@ -128,7 +172,9 @@ def test_authenticated_float_pickle_preserves_lexical_token(tmp_path, protocol): def test_authenticated_float_deepcopy_preserves_lexical_token(tmp_path): facts_path = tmp_path / "consumer_facts.jsonl" facts_path.write_text('{"aggregate_fact_key":"decimal","value":1.2500e2}\n') - value = load_ledger_consumer_artifact(facts_path).facts[0]["value"] + value = load_ledger_consumer_artifact(facts_path, lexical_floats=True).facts[0][ + "value" + ] restored = deepcopy(value) @@ -582,3 +628,38 @@ def test_real_nz_artifact_matches_committed_feed_pin(): assert artifact.fact_row_count == pin["fact_row_count"] assert artifact.schema_version == pin["artifact_schema_version"] assert list(artifact.fact_schema_versions) == pin["consumer_fact_schema_versions"] + + +def test_real_nz_lexical_opt_in_preserves_canonical_bytes(): + artifact_path = os.environ.get("NZ_CHRONICLE_ARTIFACT") + if not artifact_path: + pytest.skip("NZ_CHRONICLE_ARTIFACT is unset") + pin_path = ( + paths_for("microcosm-build").package + / "src/microcosm/build/nz/chronicle_feed.json" + ) + pin = json.loads(pin_path.read_text()) + plain = load_ledger_consumer_artifact(artifact_path, feed_pin=pin) + lexical = load_ledger_consumer_artifact( + artifact_path, feed_pin=pin, lexical_floats=True + ) + + def floats(value): + if isinstance(value, float): + yield value + elif isinstance(value, dict): + for child in value.values(): + yield from floats(child) + elif isinstance(value, (list, tuple)): + for child in value: + yield from floats(child) + + plain_values, lexical_values = ( + list(floats(plain.facts)), + list(floats(lexical.facts)), + ) + assert plain_values + assert all(type(value) is float for value in plain_values) + assert all(type(value) is LexicalFloat for value in lexical_values) + assert [float(value.lexical_token) for value in lexical_values] == plain_values + assert canonical_json(plain.facts) == canonical_json(lexical.facts) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_ledger_targets.py b/packages/microcosm-build/tests/engine_free/shared/test_ledger_targets.py index 59fe6e2246..2a9413fb65 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_ledger_targets.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_ledger_targets.py @@ -7,9 +7,14 @@ TargetCoverageRequirement, target_profile_coverage_gate, ) +from microcosm.build.ledger_artifact import ( + LexicalFloat, + load_ledger_consumer_artifact, +) from microcosm.build.ledger_targets import ( LedgerTargetMapping, LedgerTargetReference, + _strict_scalar_equal, apply_ledger_target_profile, compile_ledger_target_references, constraint_bound_shadowing_dimensions, @@ -1729,6 +1734,58 @@ def test__given_selector_matches_multiple_eligible_months__then_latest_month_is_ class TestLedgerSelectorExtensions: + @pytest.mark.parametrize( + ("actual", "expected", "equal"), + [ + (LexicalFloat("0.1"), 0.1, True), + (0.1, LexicalFloat("0.1"), True), + (LexicalFloat("0.1"), LexicalFloat("0.10"), True), + (1, 1.0, False), + (1.0, 1, False), + (True, 1, False), + (1, True, False), + (True, 1.0, False), + (1.0, True, False), + (LexicalFloat("1.0"), 1, False), + (1, LexicalFloat("1.0"), False), + (LexicalFloat("1.0"), True, False), + (True, LexicalFloat("1.0"), False), + ], + ) + def test_strict_scalar_equal_treats_lexical_floats_as_floats( + self, actual, expected, equal + ) -> None: + assert _strict_scalar_equal(actual, expected) is equal + + @pytest.mark.parametrize("lexical_floats", [False, True]) + def test_float_dimension_pin_compiles_loaded_fact( + self, tmp_path, lexical_floats + ) -> None: + facts_path = tmp_path / "consumer_facts.jsonl" + facts_path.write_text( + json.dumps(_consumer_fact_row(dimensions={"band": 0.1})) + "\n" + ) + artifact = load_ledger_consumer_artifact( + facts_path, lexical_floats=lexical_floats + ) + dimension = artifact.facts[0]["dimensions"]["band"] + assert type(dimension) is (LexicalFloat if lexical_floats else float) + reference = LedgerTargetReference( + name="float dimension selector", + ledger_selector={"dimension_values": {"band": 0.1}}, + entity="tax_unit", + measure="adjusted_gross_income", + period=2023, + family="irs_soi", + ) + + (target,) = compile_ledger_target_references( + artifact.facts, [reference], country="us" + ).specs + + assert target.name == reference.name + assert target.value == artifact.facts[0]["value"] + def test_dimension_values_use_strict_typed_equality(self) -> None: reference = LedgerTargetReference( name="typed dimension selector", diff --git a/packages/microcosm-build/tests/engine_free/shared/test_nz_reference_aging.py b/packages/microcosm-build/tests/engine_free/shared/test_nz_reference_aging.py index 448085b354..136e81bacb 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_nz_reference_aging.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_nz_reference_aging.py @@ -62,7 +62,7 @@ def artifact(): root = os.environ.get("NZ_CHRONICLE_ARTIFACT") if not root: pytest.skip("NZ_CHRONICLE_ARTIFACT is not set") - loaded = load_ledger_consumer_artifact(Path(root)) + loaded = load_ledger_consumer_artifact(Path(root), lexical_floats=True) validate_chronicle_feed_pin(loaded, _document()["feed_identity"]) return loaded diff --git a/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py b/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py index 3a561ac84c..6a30848d75 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py @@ -309,9 +309,9 @@ def rulespec_commit_mentions(value: object) -> set[str]: "rulespec" in any case); - the value, at any depth, of a key that contains "commit" in any case, unless the key's mapping is a foreign record (its repository - attribution lacks "rulespec", or a complete Chronicle identity below - ``feed_identity``) and neither the key nor an enclosing - key contains "rulespec". + attribution lacks "rulespec") or the key is one of the seven fields + of a complete Chronicle identity below ``feed_identity``, and neither + the key nor an enclosing key contains "rulespec". A foreign record cancels the context it sits in, except that its commit keys still count below a rulespec key. Integers count where hex strings @@ -338,33 +338,37 @@ def visit( rulespec_record = any( "rulespec" in repository.lower() for repository in repositories ) - artifact_identity = False + artifact_identity_fields: set[str] = set() if feed_identity_record and is_accepted_consumer_artifact_schema_version( node.get("artifact_schema_version") ): try: - chronicle_feed_identity(node) + artifact_identity_fields = set(chronicle_feed_identity(node)) except ValueError: pass - else: - artifact_identity = True - foreign = (named or artifact_identity) and not rulespec_record + foreign = named and not rulespec_record if rulespec_record: commit_value = True elif foreign: commit_value = False for key, child in node.items(): name = str(key).lower() - commit_key = "commit" in name and (rulespec_context or not foreign) + identity_field = key in artifact_identity_fields + field_commit_value = commit_value and ( + rulespec_context or not identity_field + ) + commit_key = "commit" in name and ( + rulespec_context or (not foreign and not identity_field) + ) visit( str(key), rulespec_context=rulespec_context, - commit_value=commit_value, + commit_value=field_commit_value, ) visit( child, rulespec_context=rulespec_context or "rulespec" in name, - commit_value=commit_value or commit_key or "rulespec" in name, + commit_value=field_commit_value or commit_key or "rulespec" in name, feed_identity_record=name == "feed_identity", ) elif isinstance(node, list): @@ -1486,6 +1490,8 @@ class TestRulespecPin: "unattributed", "incomplete", "explicit_commit", + "extra_commit", + "rulespec_extra_revision", "prose", "rulespec_context", "wrong_schema", @@ -1505,6 +1511,13 @@ def test_chronicle_identity_scope_preserves_rulespec_mentions(self, case) -> Non elif case == "explicit_commit": identity["rulespec_nz_commit"] = "c0ffee1" expected = {"c0ffee1"} + elif case == "extra_commit": + identity["commit"] = "deadbee" + expected = {"deadbee"} + elif case == "rulespec_extra_revision": + identity["revision"] = "deadbee" + payload = {"rulespec": payload} + expected = {commit, "deadbee"} elif case == "prose": identity["notes"] = "RuleSpec-nz deadbee" expected = {"deadbee"} diff --git a/packages/microcosm-build/tests/engine_free/shared/test_reference_factors.py b/packages/microcosm-build/tests/engine_free/shared/test_reference_factors.py index 73f3a1c0ae..2c9d98ebf4 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_reference_factors.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_reference_factors.py @@ -5,7 +5,7 @@ from fractions import Fraction import pytest -from hypothesis import given, settings +from hypothesis import example, given, settings from hypothesis import strategies as st from microcosm_build.reference_factors import factor_context_fixture, factor_fixture @@ -17,6 +17,7 @@ resolve_reference_factor, validate_factor_context, ) +from microcosm.graph.canonical import canonical_json def _fraction(row): @@ -39,7 +40,10 @@ def _oracle_root(radicand, degree, places=50): low = middle else: high = middle - return Fraction(low, denominator), Fraction(high, denominator) + lower = Fraction(low, denominator) + if lower**degree == radicand: + return lower, lower + return lower, Fraction(high, denominator) def _oracle_round(number, digits): @@ -52,22 +56,35 @@ def _oracle_round(number, digits): return quotient -def _oracle_factor(source, anchor, growth, frequency, digits): - low, high = _oracle_root(1 + Fraction(growth) / 100, frequency) - lower = ( - Fraction(anchor) - / Fraction(source) - * sum(low**index for index in range(frequency)) - / frequency - ) - upper = ( - Fraction(anchor) - / Fraction(source) - * sum(high**index for index in range(frequency)) - / frequency +def _oracle_factor(source, anchor, growth, frequency, digits, *, places=50): + while places <= 4096: + low, high = _oracle_root(1 + Fraction(growth) / 100, frequency, places) + lower = ( + Fraction(anchor) + / Fraction(source) + * sum(low**index for index in range(frequency)) + / frequency + ) + upper = ( + Fraction(anchor) + / Fraction(source) + * sum(high**index for index in range(frequency)) + / frequency + ) + if _oracle_round(lower, digits) == _oracle_round(upper, digits): + return _oracle_round(lower, digits), (low, high) + places = max(1, places * 2) + raise AssertionError("Independent oracle could not enclose a rounded factor.") + + +def test_independent_oracle_refines_ambiguous_rounding(): + coarse = _oracle_root(Fraction(103, 100), 2, places=1) + assert _oracle_round((1 + coarse[0]) / 2, 6) != _oracle_round( + (1 + coarse[1]) / 2, 6 ) - assert _oracle_round(lower, digits) == _oracle_round(upper, digits) - return _oracle_round(lower, digits), (low, high) + expected, bounds = _oracle_factor("1", "1", "3", 2, 6, places=1) + assert expected == 1007445 + assert bounds[1] - bounds[0] < coarse[1] - coarse[0] def test_factor_resolves_exact_operands_once_and_replays(): @@ -242,7 +259,7 @@ def test_closed_declaration_guards(field, mutation, message): ("coverage", "coverage mismatch"), ("boolean", "Boolean"), ("nan", "finite"), - ("float_without_token", "lexical numeric token"), + ("float_without_token", "lexical_floats=True"), ("lossy_float", "does not reproduce"), ("negative", "positive"), ], @@ -479,6 +496,7 @@ def test_receipt_tampering_refuses_and_unselected_edits_are_local(): @settings(max_examples=70, deadline=None) +@example(source=32, anchor=1, growth=0, frequency=2, digits=4) @given( source=st.integers(1, 10**9), anchor=st.integers(1, 10**9), @@ -533,3 +551,51 @@ def test_very_large_integer_cross_products_do_not_overflow(): factor, receipt = resolve_reference_factor(declaration, facts, identity) assert factor["numerator"] == 3000000 assert _fraction(receipt["denominator_mean"]) == huge + + +@pytest.mark.parametrize( + "frequency,digits,message", + [ + (13, 6, "subperiods_per_year.*1 to 12.*serializable"), + (365, 6, "subperiods_per_year.*1 to 12.*serializable"), + (12, 19, "rounding digits.*0 to 18.*serializable"), + ], +) +def test_unsupported_exact_receipt_schedule_refuses_at_declaration( + frequency, digits, message +): + declaration, facts, identity = factor_fixture() + declaration["subperiods_per_year"] = frequency + declaration["rounding"]["digits"] = digits + # A malformed schedule is intentional: range validation must precede it. + declaration["source_schedule"] = [] + with pytest.raises(ValueError, match=message): + resolve_reference_factor(declaration, facts, identity) + + +def test_largest_supported_schedule_and_rounding_have_serializable_exact_receipt(): + expected, _ = _oracle_factor("100", "110", "3", 12, 18) + declaration, facts, identity = factor_fixture( + frequency=12, growth="3", digits=18, expected_numerator=expected + ) + factor, receipt = resolve_reference_factor(declaration, facts, identity) + assert factor == {"numerator": expected, "denominator": 10**18} + encoded = canonical_json(receipt) + assert encoded and factor_document_sha256(receipt) + replay_reference_factor(declaration, facts, identity, factor, receipt) + + +def test_root_refinement_stays_within_exact_receipt_precision_budget(monkeypatch): + precisions = [] + + def ambiguous_bounds(value, degree, decimal_places): + precisions.append(decimal_places) + return Fraction(1), Fraction(2), False + + monkeypatch.setattr( + "microcosm.build.reference_factors.certified_nth_root_bounds", ambiguous_bounds + ) + declaration, facts, identity = factor_fixture(frequency=12, digits=18) + with pytest.raises(ValueError, match="supported exact receipt precision"): + resolve_reference_factor(declaration, facts, identity) + assert precisions == [26, 52, 104, 208] diff --git a/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py b/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py index c803dc5bb9..94e339494d 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py @@ -608,3 +608,114 @@ def test_self_consistent_effective_money_tampering_still_refuses(): receipt["receipt_sha256"] = reference_document_sha256(receipt) with pytest.raises(ValueError, match="must be applied exactly once"): validate_reference_resolution(effective, receipt) + + +@pytest.mark.parametrize("target", ["count", "money"]) +def test_rehashed_factor_application_cannot_bypass_companion_role(target): + references, declarations, context = _scaling_inputs() + effective, _, receipt = resolve_reference_profiles( + references, declarations, {}, factor_context=context + ) + row = next( + row for row in effective["target_references"] if row["name"] == f"{target}_0" + ) + row["value_operation"] = "linear_combination" + row["value_operands"] = [ + { + "aggregate_fact_key": row["ledger_selector"]["aggregate_fact_key"], + "expected_member_count": 1, + "weight": 2, + } + ] + if target == "money": + receipt["transformations"][row["name"]]["operation"] = "identity" + receipt["effective_references_sha256"] = reference_document_sha256(effective) + receipt.pop("receipt_sha256") + receipt["receipt_sha256"] = reference_document_sha256(receipt) + with pytest.raises(ValueError, match="roster role|count value semantics"): + validate_reference_resolution(effective, receipt) + + +def test_count_preserves_original_weighted_operation_and_refuses_changed_weight(): + references, declarations, context = _scaling_inputs() + source = references["target_references"][0] + source["value_operation"] = "linear_combination" + source["value_operands"] = [ + { + "aggregate_fact_key": source["ledger_selector"]["aggregate_fact_key"], + "expected_member_count": 1, + "weight": 1, + } + ] + effective, _, receipt = resolve_reference_profiles( + references, declarations, {}, factor_context=context + ) + row = effective["target_references"][0] + assert row["value_operation"] == source["value_operation"] + assert row["value_operands"] == source["value_operands"] + assert row["metadata"]["reference_value_kind"] == "count" + action = receipt["transformations"][row["name"]] + assert action["operation"] == "identity" + assert action["operand_selectors"] == source["value_operands"] + assert validate_reference_resolution(effective, receipt) == receipt + + row["value_operands"][0]["weight"] = 2 + action["operand_selectors"][0]["weight"] = 2 + receipt["effective_references_sha256"] = reference_document_sha256(effective) + receipt.pop("receipt_sha256") + receipt["receipt_sha256"] = reference_document_sha256(receipt) + with pytest.raises(ValueError, match="count value semantics"): + validate_reference_resolution(effective, receipt) + + +@pytest.mark.parametrize( + "mutation", + [ + "count_action", + "count_kind", + "outside_roster", + "outside_roster_without_factor", + "source_values", + "roster", + ], +) +def test_factor_roles_and_count_semantics_use_hash_bound_source_documents(mutation): + references, declarations, context = _scaling_inputs() + effective, _, receipt = resolve_reference_profiles( + references, declarations, {}, factor_context=context + ) + row = effective["target_references"][0] + if mutation == "count_action": + receipt["transformations"][row["name"]]["operation"] = "linear_combination" + elif mutation == "count_kind": + row["metadata"]["reference_value_kind"] = "money" + elif mutation == "outside_roster": + previous = row["name"] + row["name"] = "unlisted_count" + receipt["transformations"][row["name"]] = receipt["transformations"].pop( + previous + ) + receipt["transformed_names"] = sorted(receipt["transformations"]) + elif mutation == "outside_roster_without_factor": + extra = copy.deepcopy(row) + extra["name"] = "unlisted_count" + effective["target_references"].append(extra) + receipt["transformations"][extra["name"]] = copy.deepcopy( + receipt["transformations"][row["name"]] + ) + receipt["transformations"][extra["name"]].pop("factor_id") + receipt["transformed_names"] = sorted(receipt["transformations"]) + elif mutation == "source_values": + receipt["base_reference_document"]["target_references"][0]["value_operands"] = [ + {"aggregate_fact_key": "count-fact-0", "weight": 2} + ] + elif mutation == "roster": + transform = receipt["selected_declarations"]["aging_index"]["declaration"][ + "transformations" + ][0] + transform["count_reference_names"][0] = "unlisted_count" + receipt["effective_references_sha256"] = reference_document_sha256(effective) + receipt.pop("receipt_sha256") + receipt["receipt_sha256"] = reference_document_sha256(receipt) + with pytest.raises(ValueError, match="roster|source.*identity|source semantics"): + validate_reference_resolution(effective, receipt) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_transport_target_kernels.py b/packages/microcosm-build/tests/engine_free/shared/test_transport_target_kernels.py index 426c3d88ab..0dc1f6e823 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_transport_target_kernels.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_transport_target_kernels.py @@ -31,6 +31,7 @@ import microcosm.build.reference_factors as reference_factors_module import microcosm.build.reference_profiles as reference_profiles_module +import microcosm.build.transport.target_kernels as target_kernels_module from microcosm.build.ledger_artifact import ChronicleFeedPinMismatchError from microcosm.build.reference_profiles import ( reference_document_sha256, @@ -317,6 +318,80 @@ def test_compile_replay_refuses_tampered_evidence_with_recomputed_hashes(tmp_pat ) +@pytest.mark.parametrize("target", ["count", "money"]) +def test_compile_replay_refuses_rehashed_factor_application_tampering(tmp_path, target): + path, pin, document, receipt, _ = factor_transport_fixture(tmp_path, activate=True) + row = next( + row for row in document["target_references"] if row["name"] == f"toy-{target}" + ) + row["value_operation"] = "linear_combination" + row["value_operands"] = [ + { + "aggregate_fact_key": row["ledger_selector"]["aggregate_fact_key"], + "expected_member_count": 1, + "weight": 2, + } + ] + if target == "money": + receipt["transformations"][row["name"]]["operation"] = "identity" + receipt["effective_references_sha256"] = reference_document_sha256(document) + receipt.pop("receipt_sha256") + receipt["receipt_sha256"] = reference_document_sha256(receipt) + with pytest.raises(ValueError, match="roster role|count value semantics"): + compile_target_surface( + path, + document, + country=COUNTRY, + references_sha256=reference_document_sha256(document), + feed_pin=pin, + reference_resolution=receipt, + ) + + +@pytest.mark.parametrize( + "resolved", [False, True], ids=["unresolved", "legacy-resolution"] +) +def test_no_factor_compilation_is_identical_with_or_without_lexical_loading( + tmp_path, monkeypatch, resolved +): + facts = toy_facts(CALIBRATION_TARGETS) + path = write_facts(tmp_path / "facts.jsonl", facts) + document = references_for(CALIBRATION_TARGETS) + receipt = None + if resolved: + document, _, receipt = resolve_reference_profiles( + document, {"schema_version": 1, "dimensions": {}}, {} + ) + original_loader = target_kernels_module.load_ledger_consumer_artifact + requests = [] + outputs = [] + registries = [] + for force_lexical in (False, True): + + def load_with_selected_representation(*args, lexical=force_lexical, **kwargs): + requests.append(kwargs.get("lexical_floats", False)) + kwargs["lexical_floats"] = lexical + return original_loader(*args, **kwargs) + + monkeypatch.setattr( + target_kernels_module, + "load_ledger_consumer_artifact", + load_with_selected_representation, + ) + payload, registry = compile_target_surface( + path, + document, + country=COUNTRY, + references_sha256=reference_document_sha256(document), + reference_resolution=receipt, + ) + outputs.append(payload) + registries.append([spec.value for spec in registry.specs]) + assert requests == [False, False] + assert outputs[0] == outputs[1] + assert registries[0] == registries[1] + + def test_compile_emits_a_traced_surface_bound_to_facts_not_paths(tmp_path) -> None: payload = _surface(tmp_path) surface = decode_target_surface(payload) diff --git a/test_support/microcosm_build/reference_factor_transport.py b/test_support/microcosm_build/reference_factor_transport.py index f3d539b864..1fe2f52bb4 100644 --- a/test_support/microcosm_build/reference_factor_transport.py +++ b/test_support/microcosm_build/reference_factor_transport.py @@ -97,7 +97,7 @@ def factor_transport_fixture( manifest["source_commit"] = pin["source_commit"] manifest_path.write_text(json.dumps(manifest, sort_keys=True) + "\n") pin["manifest_sha256"] = hashlib.sha256(manifest_path.read_bytes()).hexdigest() - artifact = load_ledger_consumer_artifact(path, feed_pin=pin) + artifact = load_ledger_consumer_artifact(path, feed_pin=pin, lexical_floats=True) identity = chronicle_feed_identity(pin) declaration, _, _ = factor_fixture(feed_identity=identity) evidence_facts = artifact.facts diff --git a/tools/generate_nz_reference_aging.py b/tools/generate_nz_reference_aging.py index e40961b972..1a4ce04d12 100644 --- a/tools/generate_nz_reference_aging.py +++ b/tools/generate_nz_reference_aging.py @@ -227,7 +227,7 @@ def main(): args = parser.parse_args() if not args.artifact: parser.error("--artifact or NZ_CHRONICLE_ARTIFACT is required") - artifact = load_ledger_consumer_artifact(args.artifact) + artifact = load_ledger_consumer_artifact(args.artifact, lexical_floats=True) document = build_reference_aging(artifact) encoded = json.dumps(document, ensure_ascii=False, indent=2) + "\n" if args.check: From 4f770381ee7eded181e673a70dcedeb95ec4c92b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 24 Oct 2026 06:28:13 -0400 Subject: [PATCH 03/24] Bind compact factor companions instead of embedding documents 1d5fff46 embedded the selected declarations and the whole base reference document in every v2 resolution receipt, which travels in each compile and prepare node's params: the NZ default receipt grew to 4.07 MB canonical. Those documents were trusted only through hashes in the selection identity, so a compact record bound the same way is as strong. - v2 receipts carry factor_companions: per roster name, its role, factor, profile and source value semantics (474 names, 392 KB). factor_companions_sha256 and transformed_names join the v2 selection identity. - Roles still come from the bound record, never the action label; money operands and count semantics are checked as before. - The NZ default receipt is now 1.67 MB canonical, capped by a test at 2.6 MB. v1 receipt bytes are unchanged and pinned. Tests: 15 files, 2,508 passed and 5 skipped; three mutants killed. Co-Authored-By: GPT-6.1 Sol Co-Authored-By: Claude Opus 5.5 --- .../src/microcosm/build/reference_profiles.py | 132 +++++++---- .../shared/test_reference_profiles.py | 209 +++++++++++++++++- .../shared/test_transport_target_kernels.py | 77 +++++++ 3 files changed, 369 insertions(+), 49 deletions(-) diff --git a/packages/microcosm-build/src/microcosm/build/reference_profiles.py b/packages/microcosm-build/src/microcosm/build/reference_profiles.py index 9c1fdebf00..c5a282ba66 100644 --- a/packages/microcosm-build/src/microcosm/build/reference_profiles.py +++ b/packages/microcosm-build/src/microcosm/build/reference_profiles.py @@ -65,6 +65,8 @@ def _selection_identity(receipt: Mapping) -> dict: } if receipt.get("resolver_version") == FACTOR_REFERENCE_PROFILE_RESOLVER_VERSION: identity["selected_factor_identities"] = receipt["selected_factor_identities"] + identity["factor_companions_sha256"] = receipt["factor_companions_sha256"] + identity["transformed_names"] = receipt["transformed_names"] return identity @@ -73,7 +75,15 @@ def validate_reference_factor_evidence(receipt: Mapping) -> dict: from microcosm.build.reference_factors import validate_factor_context if receipt.get("resolver_version") == REFERENCE_PROFILE_RESOLVER_VERSION: - if "selected_factors" in receipt or "selected_factor_identities" in receipt: + if any( + key in receipt + for key in ( + "selected_factors", + "selected_factor_identities", + "factor_companions", + "factor_companions_sha256", + ) + ): raise ValueError("Legacy resolution cannot carry factor evidence.") return {} factors = _mapping(receipt.get("selected_factors"), "selected factors") @@ -186,6 +196,43 @@ def _validate_companions(transform: Mapping, by_name: Mapping) -> tuple[list, li return money, counts +def _factor_companions(chosen: Mapping, by_name: Mapping) -> dict: + """Capture roster roles and source value semantics before transforming rows.""" + companions = {} + for dimension, choice in chosen.items(): + for transform in choice["declaration"].get("transformations", []): + if transform.get("operation") != SCALE_MONEY_AND_INTERVALS: + continue + money, counts = _validate_companions(transform, by_name) + for role, names in (("money", money), ("count", counts)): + for name in names: + if name in companions: + raise ValueError("Duplicate factor companion roster entry.") + row = by_name[name] + companions[name] = { + "role": role, + "factor_id": transform["factor_id"], + "dimension": dimension, + "profile": choice["name"], + "source": _copy( + { + "ledger_selector": row["ledger_selector"], + "period": row.get("period"), + "value_operation": row.get( + "value_operation", "identity" + ), + "value_operands": row.get("value_operands", []), + "metadata": { + "reference_value_kind": row["metadata"][ + "reference_value_kind" + ] + }, + } + ), + } + return companions + + def _satisfy_factor_prerequisite(row: dict, dimension: str, binding: str) -> list: metadata = row.setdefault("metadata", {}) if metadata.get("activation_status") != REQUIRES_PREREQUISITES: @@ -310,10 +357,22 @@ def resolve_reference_profiles( "selection": resolved_selection, } if selected_factors: + companions = _factor_companions(chosen, by_name) receipt.update( resolver_version=FACTOR_REFERENCE_PROFILE_RESOLVER_VERSION, - selected_declarations=_copy(chosen), - base_reference_document=_copy(base), + factor_companions=companions, + factor_companions_sha256=reference_document_sha256(companions), + transformed_names=sorted( + name + for choice in chosen.values() + for transform in choice["declaration"].get("transformations", []) + for field in ( + ("reference_names", "count_reference_names") + if transform.get("operation") == SCALE_MONEY_AND_INTERVALS + else ("reference_names",) + ) + for name in _explicit_names(transform, field) + ), selected_factors=_copy(selected_factors), selected_factor_identities={ factor_id: { @@ -513,43 +572,30 @@ def resolve_reference_profiles( return effective, intervals, receipt -def _factor_reference_bindings(resolution: Mapping) -> tuple[dict, set]: - """Recover each factor row's role and source behavior from bound documents.""" +def _factor_reference_bindings(resolution: Mapping) -> dict: + """Validate the compact roster bound into the v2 selection identity.""" if resolution["resolver_version"] == REFERENCE_PROFILE_RESOLVER_VERSION: - return {}, set() - chosen = _mapping(resolution.get("selected_declarations"), "selected declarations") - base = _mapping(resolution.get("base_reference_document"), "base references") - if reference_document_sha256(chosen) != resolution.get( - "selected_declarations_sha256" - ) or reference_document_sha256(base) != resolution.get("base_references_sha256"): - raise ValueError("Factor companion source document identity mismatch.") - if { - dimension: choice.get("name") for dimension, choice in chosen.items() - } != resolution.get("selection"): - raise ValueError("Selected profile declaration identity mismatch.") - source_rows = _sequence(base.get("target_references"), "base target references") - by_name = {row["name"]: row for row in source_rows} - if len(by_name) != len(source_rows): - raise ValueError("Base reference names must be unique.") - bindings, other_names = {}, set() - for dimension, choice in chosen.items(): - for transform in choice["declaration"].get("transformations", []): - if transform.get("operation") != SCALE_MONEY_AND_INTERVALS: - other_names.update(_explicit_names(transform, "reference_names")) - continue - money, counts = _validate_companions(transform, by_name) - for role, names in (("money", money), ("count", counts)): - for name in names: - if name in bindings: - raise ValueError("Duplicate factor companion roster entry.") - bindings[name] = { - "dimension": dimension, - "profile": choice["name"], - "factor_id": transform["factor_id"], - "role": role, - "source": by_name[name], - } - return bindings, other_names + return {} + bindings = _mapping(resolution.get("factor_companions"), "factor companions") + if reference_document_sha256(bindings) != resolution.get( + "factor_companions_sha256" + ): + raise ValueError("Factor companion source record identity mismatch.") + for name, raw_binding in bindings.items(): + binding = _mapping(raw_binding, "factor companion") + source = _mapping(binding.get("source"), "factor companion source") + if ( + not isinstance(name, str) + or not name + or binding.get("role") not in {"money", "count"} + or binding.get("profile") + != resolution["selection"].get(binding.get("dimension")) + or source.get("metadata", {}).get("reference_value_kind") != binding["role"] + ): + raise ValueError( + "Factor companion roster role or profile identity mismatch." + ) + return bindings def validate_reference_resolution( @@ -586,9 +632,13 @@ def validate_reference_resolution( interval_document ) != resolution.get("effective_intervals_sha256"): raise ValueError("Effective interval document hash mismatch.") - factor_bindings, other_names = _factor_reference_bindings(resolution) - names, used_factors = [], set() + factor_bindings = _factor_reference_bindings(resolution) + other_names = set(resolution["transformed_names"]) - set(factor_bindings) + names, used_factors, seen_names = [], set(), set() for row in document.get("target_references", ()): + if factors and row["name"] in seen_names: + raise ValueError("Duplicate factor companion reference roster name.") + seen_names.add(row["name"]) metadata = row.get("metadata", {}) if metadata.get("reference_profiles_resolved"): names.append(row["name"]) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py b/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py index 94e339494d..d2bff8567d 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py @@ -16,6 +16,8 @@ resolve_reference_profiles, validate_reference_resolution, ) +from microcosm.graph.canonical import canonical_json +from test_support.paths import paths_for def _selector(key, category=None): @@ -434,6 +436,7 @@ def test_counts_are_invariant_and_every_money_row_uses_one_common_factor( "mismatched_filter", "overlap", "duplicate", + "duplicate_count", "source_period", "target_period", "diagnostic_factor", @@ -472,6 +475,8 @@ def test_invalid_factor_profiles_and_companions_refuse(mutation): ) elif mutation == "duplicate": transform["reference_names"].append(transform["reference_names"][0]) + elif mutation == "duplicate_count": + transform["count_reference_names"].append(transform["count_reference_names"][0]) elif mutation in {"source_period", "target_period"}: key = "ledger_selector" if mutation == "source_period" else "period" for row in references["target_references"]: @@ -581,6 +586,51 @@ def test_legacy_receipt_remains_version_one_without_factor_fields(): assert receipt["resolver_version"] == "reference-profiles-v1" assert "selected_factors" not in receipt assert "selected_factor_identities" not in receipt + assert "factor_companions" not in receipt + assert "factor_companions_sha256" not in receipt + # Canonical bytes produced by the v1 resolver at 1d5fff46. + assert reference_document_sha256(receipt) == ( + "753fe7ddb2bc5e00cc4d70a74e0cac8295af266ffbc05511101540e39ae21508" + ) + + +def test_nz_default_factor_receipt_is_compact_and_covers_the_exact_roster(): + spec = paths_for("microcosm-build").package / "src/microcosm/build/nz" + base = json.loads((spec / "target_references.json").read_text()) + declarations = json.loads((spec / "reference_profiles.json").read_text()) + context = json.loads((spec / "reference_aging.json").read_text()) + _, _, receipt = resolve_reference_profiles( + base, declarations, {}, factor_context=context + ) + assert "selected_declarations" not in receipt + assert "base_reference_document" not in receipt + companions = receipt["factor_companions"] + expected_names = { + f"ird_taxable_income_{kind}_by_band__row_{index}" + for kind in ("people", "total") + for index in range(8, 245) + } + assert len(companions) == 474 + assert set(companions) == expected_names + source_rows = {row["name"]: row for row in base["target_references"]} + for name, companion in companions.items(): + source = source_rows[name] + role = source["metadata"]["reference_value_kind"] + assert companion == { + "role": role, + "factor_id": "ird_ty2024_to_ty2027_central", + "dimension": "aging_index", + "profile": "central", + "source": { + "ledger_selector": source["ledger_selector"], + "period": source.get("period"), + "value_operation": source.get("value_operation", "identity"), + "value_operands": source.get("value_operands", []), + "metadata": {"reference_value_kind": role}, + }, + } + assert receipt["factor_companions_sha256"] == reference_document_sha256(companions) + assert len(canonical_json(receipt)) < 2_600_000 def test_unrelated_direct_fact_references_are_outside_companion_rosters(): @@ -636,6 +686,28 @@ def test_rehashed_factor_application_cannot_bypass_companion_role(target): validate_reference_resolution(effective, receipt) +def test_rehashed_action_label_cannot_change_money_into_a_count(): + references, declarations, context = _scaling_inputs() + effective, _, receipt = resolve_reference_profiles( + references, declarations, {}, factor_context=context + ) + row = next( + row for row in effective["target_references"] if row["name"] == "money_0" + ) + source = receipt["factor_companions"][row["name"]]["source"] + row["metadata"]["reference_value_kind"] = "count" + row["value_operation"] = source["value_operation"] + row["value_operands"] = copy.deepcopy(source["value_operands"]) + action = receipt["transformations"][row["name"]] + action["operation"] = "identity" + action["operand_selectors"] = copy.deepcopy(source["value_operands"]) + receipt["effective_references_sha256"] = reference_document_sha256(effective) + receipt.pop("receipt_sha256") + receipt["receipt_sha256"] = reference_document_sha256(receipt) + with pytest.raises(ValueError, match="roster role"): + validate_reference_resolution(effective, receipt) + + def test_count_preserves_original_weighted_operation_and_refuses_changed_weight(): references, declarations, context = _scaling_inputs() source = references["target_references"][0] @@ -675,11 +747,12 @@ def test_count_preserves_original_weighted_operation_and_refuses_changed_weight( "count_kind", "outside_roster", "outside_roster_without_factor", + "outside_roster_with_residual_label", "source_values", "roster", ], ) -def test_factor_roles_and_count_semantics_use_hash_bound_source_documents(mutation): +def test_factor_roles_and_count_semantics_use_hash_bound_companion_record(mutation): references, declarations, context = _scaling_inputs() effective, _, receipt = resolve_reference_profiles( references, declarations, {}, factor_context=context @@ -696,7 +769,10 @@ def test_factor_roles_and_count_semantics_use_hash_bound_source_documents(mutati previous ) receipt["transformed_names"] = sorted(receipt["transformations"]) - elif mutation == "outside_roster_without_factor": + elif mutation in { + "outside_roster_without_factor", + "outside_roster_with_residual_label", + }: extra = copy.deepcopy(row) extra["name"] = "unlisted_count" effective["target_references"].append(extra) @@ -704,18 +780,135 @@ def test_factor_roles_and_count_semantics_use_hash_bound_source_documents(mutati receipt["transformations"][row["name"]] ) receipt["transformations"][extra["name"]].pop("factor_id") + if mutation == "outside_roster_with_residual_label": + receipt["transformations"][extra["name"]]["operation"] = ( + "allocated_residual_by_ratio" + ) receipt["transformed_names"] = sorted(receipt["transformations"]) elif mutation == "source_values": - receipt["base_reference_document"]["target_references"][0]["value_operands"] = [ + receipt["factor_companions"][row["name"]]["source"]["value_operands"] = [ {"aggregate_fact_key": "count-fact-0", "weight": 2} ] elif mutation == "roster": - transform = receipt["selected_declarations"]["aging_index"]["declaration"][ - "transformations" - ][0] - transform["count_reference_names"][0] = "unlisted_count" + receipt["factor_companions"]["unlisted_count"] = receipt[ + "factor_companions" + ].pop(row["name"]) + receipt["effective_references_sha256"] = reference_document_sha256(effective) + receipt.pop("receipt_sha256") + receipt["receipt_sha256"] = reference_document_sha256(receipt) + with pytest.raises( + ValueError, + match=( + "roster|companion.*hash|source.*identity|source semantics" + "|selection identity mismatch" + ), + ): + validate_reference_resolution(effective, receipt) + + +@pytest.mark.parametrize("target", ["money", "count"]) +def test_rehashed_forged_companion_role_requires_the_bound_selection_identity(target): + references, declarations, context = _scaling_inputs() + effective, _, receipt = resolve_reference_profiles( + references, declarations, {}, factor_context=context + ) + row = next( + row for row in effective["target_references"] if row["name"] == f"{target}_0" + ) + companion = receipt["factor_companions"][row["name"]] + forged_role = "count" if target == "money" else "money" + companion["role"] = forged_role + companion["source"]["metadata"]["reference_value_kind"] = forged_role + row["metadata"]["reference_value_kind"] = forged_role + action = receipt["transformations"][row["name"]] + if forged_role == "money": + rational = action["operational_factor"] + operation = "linear_combination" + operands = [ + { + "aggregate_fact_key": row["ledger_selector"]["aggregate_fact_key"], + "expected_member_count": 1, + "weight": rational["numerator"] / rational["denominator"], + } + ] + else: + operation = companion["source"]["value_operation"] + operands = companion["source"]["value_operands"] + row["value_operation"] = operation + row["value_operands"] = copy.deepcopy(operands) + action["operation"] = "linear_combination" if forged_role == "money" else "identity" + action["operand_selectors"] = copy.deepcopy(operands) + # Rehash every mutable attachment while retaining the original selection. + receipt["factor_companions_sha256"] = reference_document_sha256( + receipt["factor_companions"] + ) + receipt["effective_references_sha256"] = reference_document_sha256(effective) + receipt.pop("receipt_sha256") + receipt["receipt_sha256"] = reference_document_sha256(receipt) + with pytest.raises(ValueError, match="selection identity mismatch"): + validate_reference_resolution(effective, receipt) + + +def test_factor_companion_hash_must_be_covered_by_the_selection_identity(): + references, declarations, context = _scaling_inputs() + effective, _, receipt = resolve_reference_profiles( + references, declarations, {}, factor_context=context + ) + incomplete_identity = { + key: receipt[key] + for key in ( + "resolver_version", + "base_references_sha256", + "base_intervals_sha256", + "selected_declarations_sha256", + "selection", + "selected_factor_identities", + "transformed_names", + ) + } + selection_hash = reference_document_sha256(incomplete_identity) + receipt["reference_selection_sha256"] = selection_hash + for row in effective["target_references"]: + row["metadata"]["reference_selection_sha256"] = selection_hash + receipt["effective_references_sha256"] = reference_document_sha256(effective) + receipt.pop("receipt_sha256") + receipt["receipt_sha256"] = reference_document_sha256(receipt) + with pytest.raises(ValueError, match="selection identity mismatch"): + validate_reference_resolution(effective, receipt) + + +@pytest.mark.parametrize("mutation", ["missing_record", "untransformed_record"]) +def test_factor_companion_record_and_transformed_rows_cover_each_other(mutation): + references, declarations, context = _scaling_inputs() + effective, _, receipt = resolve_reference_profiles( + references, declarations, {}, factor_context=context + ) + companions = receipt["factor_companions"] + if mutation == "missing_record": + companions.pop("count_0") + else: + companions["untransformed_count"] = copy.deepcopy(companions["count_0"]) + receipt["factor_companions_sha256"] = reference_document_sha256(companions) + # Rebind the changed record so this checks roster coverage after identity. + identity = { + key: receipt[key] + for key in ( + "resolver_version", + "base_references_sha256", + "base_intervals_sha256", + "selected_declarations_sha256", + "selection", + "selected_factor_identities", + "factor_companions_sha256", + "transformed_names", + ) + } + selection_hash = reference_document_sha256(identity) + receipt["reference_selection_sha256"] = selection_hash + for row in effective["target_references"]: + row["metadata"]["reference_selection_sha256"] = selection_hash receipt["effective_references_sha256"] = reference_document_sha256(effective) receipt.pop("receipt_sha256") receipt["receipt_sha256"] = reference_document_sha256(receipt) - with pytest.raises(ValueError, match="roster|source.*identity|source semantics"): + with pytest.raises(ValueError, match="roster|missing factor companion"): validate_reference_resolution(effective, receipt) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_transport_target_kernels.py b/packages/microcosm-build/tests/engine_free/shared/test_transport_target_kernels.py index 0dc1f6e823..e72a10d902 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_transport_target_kernels.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_transport_target_kernels.py @@ -20,6 +20,7 @@ from __future__ import annotations import json +from copy import deepcopy from dataclasses import replace from pathlib import Path @@ -348,6 +349,82 @@ def test_compile_replay_refuses_rehashed_factor_application_tampering(tmp_path, ) +@pytest.mark.parametrize("target", ["count", "money"]) +def test_compile_refuses_rehashed_companion_role_with_unchanged_selection( + tmp_path, target +): + path, pin, document, receipt, _ = factor_transport_fixture(tmp_path, activate=True) + name = f"toy-{target}" + companion = receipt["factor_companions"][name] + role = "count" if target == "money" else "money" + companion["role"] = role + companion["source"]["metadata"]["reference_value_kind"] = role + row = next(row for row in document["target_references"] if row["name"] == name) + action = receipt["transformations"][name] + # Make the forged role internally consistent. Its original selection hash + # must still refuse even when the action and effective row agree with it. + operation = "linear_combination" if role == "money" else "identity" + rational = action["operational_factor"] + operands = ( + [ + { + "aggregate_fact_key": row["ledger_selector"]["aggregate_fact_key"], + "expected_member_count": 1, + "weight": rational["numerator"] / rational["denominator"], + } + ] + if role == "money" + else [] + ) + row["metadata"]["reference_value_kind"] = role + row["value_operation"] = operation + row["value_operands"] = operands + action["operation"] = operation + action["operand_selectors"] = operands + receipt["factor_companions_sha256"] = reference_document_sha256( + receipt["factor_companions"] + ) + receipt["effective_references_sha256"] = reference_document_sha256(document) + receipt.pop("receipt_sha256") + receipt["receipt_sha256"] = reference_document_sha256(receipt) + with pytest.raises(ValueError, match="Reference selection identity mismatch"): + compile_target_surface( + path, + document, + country=COUNTRY, + references_sha256=reference_document_sha256(document), + feed_pin=pin, + reference_resolution=receipt, + ) + + +def test_compile_refuses_outside_roster_row_with_residual_action(tmp_path): + path, pin, document, receipt, _ = factor_transport_fixture(tmp_path, activate=True) + extra = deepcopy(document["target_references"][0]) + previous = extra["name"] + extra["name"] = "unlisted-count" + document["target_references"].append(extra) + action = deepcopy(receipt["transformations"][previous]) + action.pop("factor_id") + action["operation"] = "allocated_residual_by_ratio" + receipt["transformations"][extra["name"]] = action + receipt["transformed_names"] = sorted(receipt["transformations"]) + receipt["effective_references_sha256"] = reference_document_sha256(document) + receipt.pop("receipt_sha256") + receipt["receipt_sha256"] = reference_document_sha256(receipt) + with pytest.raises( + ValueError, match="selection identity|outside its companion roster" + ): + compile_target_surface( + path, + document, + country=COUNTRY, + references_sha256=reference_document_sha256(document), + feed_pin=pin, + reference_resolution=receipt, + ) + + @pytest.mark.parametrize( "resolved", [False, True], ids=["unresolved", "legacy-resolution"] ) From d28817ff0ce8a4ed5340efd639d5520d06db5f83 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 24 Oct 2026 10:16:41 -0400 Subject: [PATCH 04/24] Keep malformed resolution receipts on the ValueError path Belgium's Sol review of 4f770381 found two latent regressions in validate_reference_resolution: a receipt without transformed_names raised KeyError, and an unnamed unresolved row raised KeyError where base accepted it. transformed_names is now checked as a list of strings, and roster-name bookkeeping runs only with factor evidence. The docstring states the trust boundary: checks assume a trusted selection identity; selected factors are anchored by replay against the authenticated feed. The resolver property test suppresses only too_slow: an integer draw took 31 s under host load in review. Tests: test_legacy_receipt_without_transformed_names_refuses_as_a_value_error and test_unnamed_unresolved_rows_still_validate_without_factor_evidence; each goes red (KeyError) with its fix reverted. test_reference_profiles.py 74 passed, test_transport_target_kernels.py 52, test_nz_target_references.py 50. Co-Authored-By: Claude Opus 5.5 --- .../src/microcosm/build/reference_profiles.py | 23 +++++++++++--- .../shared/test_reference_profiles.py | 31 +++++++++++++++++-- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/packages/microcosm-build/src/microcosm/build/reference_profiles.py b/packages/microcosm-build/src/microcosm/build/reference_profiles.py index c5a282ba66..e6b6a33920 100644 --- a/packages/microcosm-build/src/microcosm/build/reference_profiles.py +++ b/packages/microcosm-build/src/microcosm/build/reference_profiles.py @@ -605,7 +605,14 @@ def validate_reference_resolution( references_sha256: str | None = None, interval_document: Mapping | None = None, ) -> dict: - """Refuse stale effective documents, receipts or interval companions.""" + """Refuse stale effective documents, receipts or interval companions. + + The checks bind the effective rows to the receipt and the receipt's + factor companions to its selection identity. They assume that identity is + trusted, as when composition resolves a fresh receipt: a forger who also + replaces the selection identity is outside this layer. Selected factors + are anchored independently by replay against the authenticated feed. + """ resolution = _copy(_mapping(receipt, "reference resolution receipt")) if ( resolution.get("resolver_version") @@ -633,12 +640,18 @@ def validate_reference_resolution( ) != resolution.get("effective_intervals_sha256"): raise ValueError("Effective interval document hash mismatch.") factor_bindings = _factor_reference_bindings(resolution) - other_names = set(resolution["transformed_names"]) - set(factor_bindings) + transformed_names = resolution.get("transformed_names") + if not isinstance(transformed_names, list) or not all( + isinstance(name, str) for name in transformed_names + ): + raise ValueError("Resolution receipt transformed names are malformed.") + other_names = set(transformed_names) - set(factor_bindings) names, used_factors, seen_names = [], set(), set() for row in document.get("target_references", ()): - if factors and row["name"] in seen_names: - raise ValueError("Duplicate factor companion reference roster name.") - seen_names.add(row["name"]) + if factors: + if row.get("name") in seen_names: + raise ValueError("Duplicate factor companion reference roster name.") + seen_names.add(row.get("name")) metadata = row.get("metadata", {}) if metadata.get("reference_profiles_resolved"): names.append(row["name"]) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py b/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py index d2bff8567d..5685bc3427 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py @@ -6,7 +6,7 @@ import json import pytest -from hypothesis import given, settings +from hypothesis import HealthCheck, given, settings from hypothesis import strategies as st from microcosm_build.reference_factors import factor_context_fixture @@ -401,7 +401,14 @@ def test_factor_scales_money_once_keeps_counts_and_measurement_prerequisite(): pair_count=st.integers(min_value=1, max_value=16), anchor=st.integers(min_value=1, max_value=500), ) -@settings(max_examples=16, database=None, deadline=None) +# Two integers are cheap to draw; a 31 s draw under host load tripped +# too_slow in review, so only wall-clock health is suppressed. +@settings( + max_examples=16, + database=None, + deadline=None, + suppress_health_check=[HealthCheck.too_slow], +) def test_counts_are_invariant_and_every_money_row_uses_one_common_factor( pair_count, anchor ): @@ -594,6 +601,26 @@ def test_legacy_receipt_remains_version_one_without_factor_fields(): ) +def test_legacy_receipt_without_transformed_names_refuses_as_a_value_error(): + references, declarations = _inputs() + effective, _, receipt = resolve_reference_profiles(references, declarations, {}) + receipt.pop("transformed_names") + receipt.pop("receipt_sha256") + receipt["receipt_sha256"] = reference_document_sha256(receipt) + with pytest.raises(ValueError, match="transformed names"): + validate_reference_resolution(effective, receipt) + + +def test_unnamed_unresolved_rows_still_validate_without_factor_evidence(): + references, declarations = _inputs() + effective, _, receipt = resolve_reference_profiles(references, declarations, {}) + effective["target_references"].append({"entity": "household", "metadata": {}}) + receipt.pop("receipt_sha256") + receipt["effective_references_sha256"] = reference_document_sha256(effective) + receipt["receipt_sha256"] = reference_document_sha256(receipt) + assert validate_reference_resolution(effective, receipt) == receipt + + def test_nz_default_factor_receipt_is_compact_and_covers_the_exact_roster(): spec = paths_for("microcosm-build").package / "src/microcosm/build/nz" base = json.loads((spec / "target_references.json").read_text()) From 2a641321edb0371f06dc75dbde6d02239431acfc Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 24 Oct 2026 18:06:35 -0400 Subject: [PATCH 05/24] Check factor currency at run entry, not in validation Factor validation compared date.today() with each declaration's validity window. Every NZ factor declares valid_until 2026-11-03, so main's CI would have failed for every PR from 2026-11-04. Validation is now a pure function of declaration and feed: declared_on <= as_of and valid_from <= as_of <= valid_until. require_current_factors(selected, *, today) has no default for today. The transport CLI calls it once, for --resume auto|forbid, with today's UTC date, before preparation can run CREATE. --resume require replays a stored graph and is exempt. No kernel reads the clock. The CLI checks every factor the default and scenario profile views select, through compose.reference_profile_views, the walk activation now shares, so a spec without scenarios checks its default view alone (Belgium's review). An end-to-end test replays a warm store under --resume require after the factor expires: rc 0, no kernel runs, and the stored export's bytes unchanged. From the d28817ff review (findings 1-4): resolution transformations must map names to objects, resolved and factor rows need nonempty names, and a v2 unresolved duplicate companion refuses. Unresolved unnamed v1 rows still validate. The expired-factor mutation in test_reference_factors now expires before its as_of, so the retained as_of check refuses it. Tests (hub, one file per process, normal priority): 15 files, 2,077 passed and 5 skipped (the UK feed cases). Clock runs at 2026-11-05: test_nz_reference_aging 18 and test_reference_profiles 97 passed. Whole CLI test file with the CLI clock at 2100-01-01: 163 passed. Mutants killed, 8 of 8: the clock back in validation, the currency check on replay, no currency check, run entry walking other views than activation, the local date, and review mutants M3, M6 and M7. Receipts, effective intervals and factor contexts are byte-identical to d28817ff (8 payloads, re-checked on this tree). Co-Authored-By: GPT-6.1 Sol Co-Authored-By: Claude Opus 5.5 --- .../src/microcosm/build/reference_factors.py | 24 ++- .../src/microcosm/build/reference_profiles.py | 27 +++- .../src/microcosm/build/transport/cli.py | 36 ++++- .../src/microcosm/build/transport/compose.py | 36 +++-- .../shared/test_nz_reference_aging.py | 34 ++++ .../shared/test_reference_factors.py | 8 +- .../shared/test_reference_profiles.py | 110 ++++++++++++- .../shared/test_transport_composed_graph.py | 147 +++++++++++++++++- 8 files changed, 397 insertions(+), 25 deletions(-) diff --git a/packages/microcosm-build/src/microcosm/build/reference_factors.py b/packages/microcosm-build/src/microcosm/build/reference_factors.py index 387c64b958..1a02dade36 100644 --- a/packages/microcosm-build/src/microcosm/build/reference_factors.py +++ b/packages/microcosm-build/src/microcosm/build/reference_factors.py @@ -233,8 +233,6 @@ def _declaration(declaration: object) -> dict: raise ValueError( "Factor declaration is expired or not yet valid at its declared as_of." ) - if not dates["valid_from"] <= date.today() <= dates["valid_until"]: - raise ValueError("Factor declaration is expired or not yet valid today.") for key in ("evidence", "decision_references"): if any(not isinstance(item, str) or not item for item in _list(row[key], key)): raise ValueError(f"{key} must contain nonempty text.") @@ -717,6 +715,28 @@ def validate_factor_context( return result +def require_current_factors(selected: Mapping[str, dict], *, today: date) -> None: + """Require current declarations from ``validate_factor_context``'s result. + + The currency check guards new runs only. Validation and replay remain pure + functions of their recorded inputs; run entry supplies the explicit date. + """ + outside_validity = [] + for factor_id, entry in selected.items(): + validity = entry["declaration"]["validity"] + if not ( + date.fromisoformat(validity["valid_from"]) + <= today + <= date.fromisoformat(validity["valid_until"]) + ): + outside_validity.append(factor_id) + if outside_validity: + raise ValueError( + f"Selected factors {sorted(outside_validity)} are expired or not yet " + f"valid on {today.isoformat()}; a reviewed re-declaration is needed." + ) + + def replay_reference_factor( declaration: Mapping, facts: Iterable, diff --git a/packages/microcosm-build/src/microcosm/build/reference_profiles.py b/packages/microcosm-build/src/microcosm/build/reference_profiles.py index e6b6a33920..10015e8afc 100644 --- a/packages/microcosm-build/src/microcosm/build/reference_profiles.py +++ b/packages/microcosm-build/src/microcosm/build/reference_profiles.py @@ -645,24 +645,35 @@ def validate_reference_resolution( isinstance(name, str) for name in transformed_names ): raise ValueError("Resolution receipt transformed names are malformed.") + transformations = _mapping( + resolution.get("transformations"), "resolution transformations" + ) + if any(not isinstance(action, Mapping) for action in transformations.values()): + raise ValueError("Resolution transformations must map names to objects.") other_names = set(transformed_names) - set(factor_bindings) names, used_factors, seen_names = [], set(), set() for row in document.get("target_references", ()): + name = row.get("name") + metadata = row.get("metadata", {}) + resolved = metadata.get("reference_profiles_resolved") + if (factors or resolved) and (not isinstance(name, str) or not name): + raise ValueError( + "Resolved and factor reference names must be nonempty strings." + ) if factors: - if row.get("name") in seen_names: + if name in seen_names: raise ValueError("Duplicate factor companion reference roster name.") - seen_names.add(row.get("name")) - metadata = row.get("metadata", {}) - if metadata.get("reference_profiles_resolved"): - names.append(row["name"]) + seen_names.add(name) + if resolved: + names.append(name) if metadata.get("reference_selection_sha256") != selection_hash or ( metadata.get("effective_intervals_sha256") != resolution.get("effective_intervals_sha256") ): raise ValueError("Effective reference profile identity mismatch.") - action = resolution.get("transformations", {}).get(row["name"], {}) - binding = factor_bindings.get(row["name"]) - if factors and binding is None and row["name"] not in other_names: + action = transformations.get(name, {}) + binding = factor_bindings.get(name) + if factors and binding is None and name not in other_names: raise ValueError( "Factor action names a row outside its companion roster." ) diff --git a/packages/microcosm-build/src/microcosm/build/transport/cli.py b/packages/microcosm-build/src/microcosm/build/transport/cli.py index 3d26dd15ff..38411e7af6 100644 --- a/packages/microcosm-build/src/microcosm/build/transport/cli.py +++ b/packages/microcosm-build/src/microcosm/build/transport/cli.py @@ -14,6 +14,10 @@ explorer page and ``build-status.json``. The status file lists the outputs the spec declares as still owed by a later package. There is no upload, staging or publication path; ``--local-only`` is the only mode. + +The currency check guards new runs only: ``auto`` and ``forbid`` require +selected factors current on today's UTC date before CREATE; ``require`` only +verifies stored work. """ from __future__ import annotations @@ -24,6 +28,7 @@ import sys from collections.abc import Mapping, Sequence from dataclasses import dataclass, replace +from datetime import UTC, date, datetime from pathlib import Path from tempfile import TemporaryDirectory from types import MappingProxyType @@ -32,6 +37,7 @@ import pandas as pd from microcosm.build.artifact_files import file_artifact, materialize_bytes +from microcosm.build.reference_factors import require_current_factors from microcosm.frame import Frame from microcosm.frame.adapters.axiom import AxiomEntityTableDataset from microcosm.frame.materialize import put_frame_table @@ -60,6 +66,7 @@ TransportGraphExtension, compose_transport_graph, load_transport_spec, + reference_profile_views, skeleton_create_declaration, transport_endpoints, transport_pending_outputs, @@ -634,8 +641,34 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: return args +def _today() -> date: + """Today's UTC date, so a local run and CI agree on a validity boundary.""" + return datetime.now(UTC).date() + + +def _require_current_run_factors( + spec: Mapping[str, object], *, resume: ResumePolicy +) -> None: + """The currency check guards new runs only, including scenario selections. + + ``require`` only replays stored work and may verify historical factors. + Check every factor the default and scenario views select, through the + walk activation uses, against today's UTC date, before CREATE. + """ + if resume == "require": + return + selected = {} + for view in reference_profile_views(spec): + selected.update(view[2].get("selected_factors", {})) + require_current_factors(selected, today=_today()) + + def main(argv: Sequence[str] | None = None) -> int: - """Run the CLI; return 0 on success and 1 on a refused build.""" + """Run the CLI; return 0 on success and 1 on a refused build. + + The currency check guards new runs only (``auto`` and ``forbid``), before + preparation can run CREATE. ``require`` verifies existing stored work. + """ try: args = parse_args(argv) @@ -656,6 +689,7 @@ def main(argv: Sequence[str] | None = None) -> int: "--resume require refuses a cold transport graph store." ) spec = load_transport_spec(args.spec_dir if args.spec_dir else args.country) + _require_current_run_factors(spec, resume=args.resume) prepared = prepare_transport_build( spec, sources=sources, inventory_store=store_root, resume=args.resume ) diff --git a/packages/microcosm-build/src/microcosm/build/transport/compose.py b/packages/microcosm-build/src/microcosm/build/transport/compose.py index 82740818d2..b0907925d3 100644 --- a/packages/microcosm-build/src/microcosm/build/transport/compose.py +++ b/packages/microcosm-build/src/microcosm/build/transport/compose.py @@ -413,6 +413,30 @@ def reference_profile_view( return view +def reference_profile_views(spec: Mapping[str, object]) -> tuple: + """Resolve the default profile view and each distinct scenario-knob view. + + Activation and run entry share this walk. Scenario views resolve only when + both ``reference_profiles`` and ``scenarios`` are declared; knob sets that + repeat, or equal the default selection, resolve once. + """ + view = reference_profile_view(spec) + if view is None: + return () + views = [view] + if _has_resource(spec, "scenarios"): + checked_selections = {canonical_json({})} + for scenario in _sequence( + _raw_transport_resource(spec, "scenarios").get("scenarios"), "scenarios" + ): + knobs = _mapping(_mapping(scenario, "scenario").get("knobs", {}), "knobs") + identity = canonical_json(knobs) + if identity not in checked_selections: + views.append(reference_profile_view(spec, knobs)) + checked_selections.add(identity) + return tuple(views) + + def transport_resource( spec: Mapping[str, object], name: str, *, profile_selection: Mapping | None = None ) -> Mapping: @@ -552,17 +576,7 @@ def validate_transport_activation(spec: Mapping[str, object]) -> None: missing = [] # Resolve first so activation never inspects an executable profile's raw rows. - reference_profile_view(spec) - if _has_resource(spec, "reference_profiles") and _has_resource(spec, "scenarios"): - checked_selections = {canonical_json({})} - for scenario in _sequence( - _raw_transport_resource(spec, "scenarios").get("scenarios"), "scenarios" - ): - knobs = _mapping(_mapping(scenario, "scenario").get("knobs", {}), "knobs") - identity = canonical_json(knobs) - if identity not in checked_selections: - reference_profile_view(spec, knobs) - checked_selections.add(identity) + reference_profile_views(spec) selections = ( tuple(_declared_resource_selections(transport_resource(spec, GRAPH_RESOURCE))) if _has_resource(spec, GRAPH_RESOURCE) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_nz_reference_aging.py b/packages/microcosm-build/tests/engine_free/shared/test_nz_reference_aging.py index 136e81bacb..fa9ecb3aca 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_nz_reference_aging.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_nz_reference_aging.py @@ -5,12 +5,14 @@ import copy import json import os +from datetime import date from decimal import Decimal, localcontext from fractions import Fraction from pathlib import Path import pytest +from microcosm.build import reference_factors from microcosm.build.ledger_artifact import ( LexicalFloat, load_ledger_consumer_artifact, @@ -19,6 +21,7 @@ from microcosm.build.ledger_targets import fact_matches_selector from microcosm.build.reference_factors import ( replay_reference_factor, + require_current_factors, resolve_reference_factor, validate_factor_context, ) @@ -92,6 +95,37 @@ def test_generated_aging_resource_reproduces_pinned_feed_and_all_receipts(artifa assert entry["receipt"]["feed_identity"] == identity +def test_packaged_factor_context_validation_ignores_the_wall_clock(monkeypatch): + class FutureDate(date): + @classmethod + def today(cls): + return cls(2026, 11, 5) + + monkeypatch.setattr(reference_factors, "date", FutureDate) + + assert set(validate_factor_context(_document())) == set(_EXPECTED) + + +@pytest.mark.parametrize( + "today,current", + [ + (date(2026, 10, 22), False), + (date(2026, 10, 23), True), + (date(2026, 11, 3), True), + (date(2026, 11, 4), False), + ], +) +def test_factor_currency_uses_inclusive_declared_boundaries(today, current): + selected = validate_factor_context(_document()) + if current: + require_current_factors(selected, today=today) + else: + with pytest.raises(ValueError, match="reviewed re-declaration") as error: + require_current_factors(selected, today=today) + for factor_id in selected: + assert factor_id in str(error.value) + + @pytest.mark.parametrize("factor_id", list(_EXPECTED)) def test_all_six_factors_and_observed_diagnostic_are_exact(factor_id, operand_facts): entry = _entry(factor_id) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_reference_factors.py b/packages/microcosm-build/tests/engine_free/shared/test_reference_factors.py index 2c9d98ebf4..6e2045bbd8 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_reference_factors.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_reference_factors.py @@ -1,6 +1,7 @@ """Exact factor arithmetic, evidence and replay without country assumptions.""" from copy import deepcopy +from datetime import date from decimal import Decimal from fractions import Fraction @@ -14,6 +15,7 @@ certified_nth_root_bounds, factor_document_sha256, replay_reference_factor, + require_current_factors, resolve_reference_factor, validate_factor_context, ) @@ -108,6 +110,10 @@ def test_factor_resolves_exact_operands_once_and_replays(): assert validate_factor_context(context, ["toy_factor"]) == {"toy_factor": entry} +def test_currency_check_without_selected_factors_is_a_noop(): + require_current_factors({}, today=date(2026, 11, 5)) + + def test_factor_resolution_and_replay_ignore_caller_feed_pin_prose(): declaration, facts, identity = factor_fixture() factor, receipt = resolve_reference_factor(declaration, facts, identity) @@ -401,7 +407,7 @@ def test_numeric_and_schedule_guards(mutation, message): elif mutation == "sensitivity": declaration["growth"]["sensitivity_provenance"] = [] elif mutation == "expired": - declaration["validity"]["valid_until"] = "2005-01-01" + declaration["validity"]["valid_until"] = "2001-12-31" elif mutation == "wrong_expected": declaration["expected_operational_factor"]["numerator"] += 1 elif mutation == "wrong_feed": diff --git a/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py b/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py index 5685bc3427..60e2cee84d 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py @@ -4,6 +4,7 @@ import copy import json +from datetime import date import pytest from hypothesis import HealthCheck, given, settings @@ -614,13 +615,100 @@ def test_legacy_receipt_without_transformed_names_refuses_as_a_value_error(): def test_unnamed_unresolved_rows_still_validate_without_factor_evidence(): references, declarations = _inputs() effective, _, receipt = resolve_reference_profiles(references, declarations, {}) - effective["target_references"].append({"entity": "household", "metadata": {}}) + effective["target_references"].extend( + [ + {"entity": "household", "metadata": {}}, + {"entity": "household", "metadata": {}}, + ] + ) receipt.pop("receipt_sha256") receipt["effective_references_sha256"] = reference_document_sha256(effective) receipt["receipt_sha256"] = reference_document_sha256(receipt) assert validate_reference_resolution(effective, receipt) == receipt +def test_duplicate_unresolved_factor_companion_refuses(): + references, declarations, context = _scaling_inputs() + effective, _, receipt = resolve_reference_profiles( + references, declarations, {}, factor_context=context + ) + duplicate = copy.deepcopy(effective["target_references"][0]) + assert duplicate["name"] == "count_0" + duplicate["metadata"].pop("reference_profiles_resolved") + effective["target_references"].append(duplicate) + receipt["effective_references_sha256"] = reference_document_sha256(effective) + receipt.pop("receipt_sha256") + receipt["receipt_sha256"] = reference_document_sha256(receipt) + with pytest.raises(ValueError, match="Duplicate factor companion"): + validate_reference_resolution(effective, receipt) + + +@pytest.mark.parametrize("with_factors", [False, True], ids=["v1", "v2"]) +@pytest.mark.parametrize( + "mutation", + [ + "transformations_list", + "transformations_null", + "transformation_scalar", + "resolved_name_missing", + "resolved_name_null", + "resolved_name_empty", + "resolved_name_scalar", + "resolved_name_list", + ], +) +def test_malformed_resolution_shapes_refuse_as_value_errors(with_factors, mutation): + if with_factors: + references, declarations, context = _scaling_inputs() + effective, _, receipt = resolve_reference_profiles( + references, declarations, {}, factor_context=context + ) + else: + references, declarations = _inputs() + effective, _, receipt = resolve_reference_profiles(references, declarations, {}) + row = effective["target_references"][0] + if mutation == "transformations_list": + receipt["transformations"] = [] + elif mutation == "transformations_null": + receipt["transformations"] = None + elif mutation == "transformation_scalar": + receipt["transformations"][row["name"]] = 7 + elif mutation == "resolved_name_missing": + row.pop("name") + else: + row["name"] = { + "resolved_name_null": None, + "resolved_name_empty": "", + "resolved_name_scalar": 7, + "resolved_name_list": [], + }[mutation] + receipt["effective_references_sha256"] = reference_document_sha256(effective) + receipt.pop("receipt_sha256") + receipt["receipt_sha256"] = reference_document_sha256(receipt) + with pytest.raises(ValueError, match="transformations|reference names"): + validate_reference_resolution(effective, receipt) + + +@pytest.mark.parametrize( + "name_fields", + [{}, {"name": None}, {"name": ""}, {"name": 7}, {"name": []}], + ids=["missing", "null", "empty", "int", "list"], +) +def test_unnamed_unresolved_factor_rows_refuse_as_value_errors(name_fields): + references, declarations, context = _scaling_inputs() + effective, _, receipt = resolve_reference_profiles( + references, declarations, {}, factor_context=context + ) + effective["target_references"].append( + {"entity": "household", "metadata": {}, **name_fields} + ) + receipt["effective_references_sha256"] = reference_document_sha256(effective) + receipt.pop("receipt_sha256") + receipt["receipt_sha256"] = reference_document_sha256(receipt) + with pytest.raises(ValueError, match="reference names"): + validate_reference_resolution(effective, receipt) + + def test_nz_default_factor_receipt_is_compact_and_covers_the_exact_roster(): spec = paths_for("microcosm-build").package / "src/microcosm/build/nz" base = json.loads((spec / "target_references.json").read_text()) @@ -660,6 +748,26 @@ def test_nz_default_factor_receipt_is_compact_and_covers_the_exact_roster(): assert len(canonical_json(receipt)) < 2_600_000 +def test_nz_default_factor_resolution_validates_after_declaration_expiry(monkeypatch): + from microcosm.build import reference_factors + + class LaterDate(date): + @classmethod + def today(cls): + return cls(2026, 11, 5) + + monkeypatch.setattr(reference_factors, "date", LaterDate) + spec = paths_for("microcosm-build").package / "src/microcosm/build/nz" + base = json.loads((spec / "target_references.json").read_text()) + declarations = json.loads((spec / "reference_profiles.json").read_text()) + context = json.loads((spec / "reference_aging.json").read_text()) + effective, _, receipt = resolve_reference_profiles( + base, declarations, {}, factor_context=context + ) + assert receipt["selected_factors"] + assert validate_reference_resolution(effective, receipt) == receipt + + def test_unrelated_direct_fact_references_are_outside_companion_rosters(): references, declarations, context = _scaling_inputs() direct = { diff --git a/packages/microcosm-build/tests/engine_free/shared/test_transport_composed_graph.py b/packages/microcosm-build/tests/engine_free/shared/test_transport_composed_graph.py index 5d6ee3a416..4187358f15 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_transport_composed_graph.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_transport_composed_graph.py @@ -43,6 +43,7 @@ import shutil import time from dataclasses import replace +from datetime import UTC, date, datetime, timedelta, timezone from pathlib import Path from unittest.mock import Mock @@ -1454,7 +1455,10 @@ def test_packaged_spec_refuses_before_reading_any_source(tmp_path): ) -def test_cli_main_refuses_the_packaged_spec_and_writes_nothing(tmp_path, capsys): +def test_cli_main_refuses_the_packaged_spec_and_writes_nothing( + tmp_path, monkeypatch, capsys +): + monkeypatch.setattr(cli_module, "_today", lambda: date(2026, 10, 24)) out = tmp_path / "out" assert cli_module.main(["--country", "nz", "--out", str(out)]) == 1 message = capsys.readouterr().err @@ -1643,6 +1647,50 @@ def injected_registry(bindings, root, *, unit_rule, engines_by_binding=None): assert not (out / EXPORT_FILENAME).exists() +_AFTER_FACTOR_EXPIRY = date(2100, 1, 1) + + +@pytest.mark.parametrize("resume", ["auto", "forbid"]) +@pytest.mark.parametrize("selection", ["default", "scenario"]) +def test_cli_main_refuses_expired_factors_before_create( + tmp_path, monkeypatch, capsys, resume, selection +): + _, _, _, _, spec = factor_transport_fixture(tmp_path) + if selection == "scenario": + spec["resources"]["reference_profiles"]["dimensions"]["index"]["profiles"][ + "central" + ]["transformations"] = [] + spec["resources"]["scenarios"]["scenarios"].append( + {"id": "variant", "knobs": {"index": "low"}} + ) + monkeypatch.setattr(cli_module, "_today", lambda: _AFTER_FACTOR_EXPIRY) + monkeypatch.setattr(cli_module, "load_transport_spec", lambda source: spec) + prepare = Mock(side_effect=AssertionError("Must refuse before preparation.")) + create = Mock(side_effect=AssertionError("Must refuse before CREATE.")) + monkeypatch.setattr(cli_module, "prepare_transport_build", prepare) + monkeypatch.setattr(cli_module.TRANSPORT_CREATE, "run", create) + out = tmp_path / "out" + assert ( + cli_module.main(["--country", "xx", "--out", str(out), "--resume", resume]) == 1 + ) + error = capsys.readouterr().err + assert "toy_factor" in error + assert "reviewed re-declaration" in error + prepare.assert_not_called() + create.assert_not_called() + assert not out.exists() + + +def test_cli_factor_currency_decision_allows_historical_replay(tmp_path, monkeypatch): + # The factor fixture has parked measurement rows and no runnable skeleton. + # Exercise the CLI decision directly; the warm-store test below verifies + # actual require execution with the engine-free composed skeleton. + _, _, _, _, spec = factor_transport_fixture(tmp_path) + assert compose_module.reference_profile_view(spec)[2]["selected_factors"] + monkeypatch.setattr(cli_module, "_today", lambda: _AFTER_FACTOR_EXPIRY) + cli_module._require_current_run_factors(spec, resume="require") + + def test_cli_main_and_path_loader_success(composed, tmp_path, monkeypatch, capsys): # Real Path loading, registry preparation and execution; only the injected # engine seam replaces Axiom with the fixture's pure-Python adapters. @@ -1691,8 +1739,105 @@ def counted(context, _run=run, _ref=ref): assert (out / EXPORT_FILENAME).is_file() assert json.loads((out / "manifest.json").read_bytes())["nodes"] observe_warm.append(True) + monkeypatch.setattr(cli_module, "_today", lambda: _AFTER_FACTOR_EXPIRY) + assert cli_module.main([*argv, "--resume", "require"]) == 0 + assert capsys.readouterr().err == "" + assert calls == [] + + +def test_cli_run_date_is_the_utc_date(monkeypatch): + # 11:30pm in New York on 3 November is already 4 November in UTC, where CI runs. + instant = datetime(2026, 11, 4, 3, 30, tzinfo=UTC) + + class Clock(datetime): + @classmethod + def now(cls, tz=None): + return instant.astimezone(tz or timezone(timedelta(hours=-5))) + + monkeypatch.setattr(cli_module, "datetime", Clock) + assert cli_module._today() == date(2026, 11, 4) + + +def test_cli_currency_check_walks_the_views_activation_walks(tmp_path, monkeypatch): + # Activation resolves scenario views only when both resources exist and + # validates their shapes; the run-entry check shares that walk. + _, _, _, _, spec = factor_transport_fixture(tmp_path) + del spec["resources"]["scenarios"] + assert len(compose_module.reference_profile_views(spec)) == 1 + monkeypatch.setattr(cli_module, "_today", lambda: date(2050, 1, 1)) + cli_module._require_current_run_factors(spec, resume="auto") + monkeypatch.setattr(cli_module, "_today", lambda: _AFTER_FACTOR_EXPIRY) + with pytest.raises(ValueError, match="toy_factor"): + cli_module._require_current_run_factors(spec, resume="auto") + spec["resources"]["scenarios"] = {"scenarios": {"id": "malformed"}} + with pytest.raises(TypeError) as activation: + compose_module.validate_transport_activation(spec) + with pytest.raises(TypeError) as run_entry: + cli_module._require_current_run_factors(spec, resume="auto") + assert str(run_entry.value) == str(activation.value) == "scenarios must be a list." + + +def test_cli_require_replays_a_warm_store_after_selected_factors_expire( + composed, tmp_path, monkeypatch, capsys +): + # End to end: warm a real store while a selected factor is current, then + # replay it past valid_until. require verifies and runs nothing; a new run + # of the same spec refuses on that factor before preparation. + _, _, _, _, factors = factor_transport_fixture(tmp_path / "factor") + spec = copy.deepcopy(composed.spec) + spec["resources"].update( + factor_target_references=factors["resources"]["target_references"], + factor_context=factors["resources"]["factor_context"], + reference_profiles={ + **factors["resources"]["reference_profiles"], + "reference_resource": "factor_target_references", + }, + ) + assert compose_module.reference_profile_view(spec)[2]["selected_factors"] + directory = _write_local_transport_spec(tmp_path / "spec", spec) + build_registry = cli_module.build_transport_registry + observe_warm = [] + calls = [] + + def injected_registry(bindings, root, *, unit_rule, engines_by_binding=None): + registry = build_registry( + bindings, + root, + unit_rule=unit_rule, + engines_by_binding=composed.engines_by_binding, + ) + if observe_warm: + for ref in registry.kernels.refs(): + kernel = registry.kernels.get(ref) + run = kernel.run + + def counted(context, _run=run, _ref=ref): + calls.append(_ref) + return _run(context) + + monkeypatch.setattr(kernel, "run", counted) + return registry + + monkeypatch.setattr(cli_module, "build_transport_registry", injected_registry) + out = tmp_path / "out" + argv = ["--spec-dir", str(directory), "--out", str(out)] + for name, path in composed.sources.items(): + argv.extend(["--source", f"{name}={path}"]) + monkeypatch.setattr(cli_module, "_today", lambda: date(2050, 1, 1)) + assert cli_module.main(argv) == 0 + assert capsys.readouterr().err == "" + exported = (out / EXPORT_FILENAME).read_bytes() + observe_warm.append(True) + monkeypatch.setattr(cli_module, "_today", lambda: _AFTER_FACTOR_EXPIRY) assert cli_module.main([*argv, "--resume", "require"]) == 0 assert capsys.readouterr().err == "" + # require reads back the stored run's export and rewrites nothing. + assert (out / EXPORT_FILENAME).read_bytes() == exported + assert calls == [] + assert cli_module.main([*argv, "--resume", "auto"]) == 1 + error = capsys.readouterr().err + assert "toy_factor" in error + assert "reviewed re-declaration" in error assert calls == [] From 75c2768889a1958950c2150f50413977d774cead Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 24 Oct 2026 18:44:54 -0400 Subject: [PATCH 06/24] Add exact income-band intervals and boundary diagnostics Add a shared reference_intervals module: per-table declared semantics (band closure, nil band, negative-income policy) with no defaults, exact rational cuts on signed integer-cent inputs, classification, scaling and diagnostics. Interval tables scale once with their companions' selected factor, keeping fractional-cent cuts exact. Bind interval outputs as their own kind in calibration_columns, beside predicate outputs, and leave roster preparation untouched. Preparation receives the exact effective interval companion, recomputes indicators from each variant's cuts and records input fingerprints; the problem kernel re-derives them and adds fixed-weight boundary diagnostics to its metadata, never to targets. Declare NZ's 237 national and 183 age bands, checked against every feed label, and scale the national table in all three ageing profiles. All 474 IRD and 235 P1 rows stay parked. fix3's expiry test now passes the interval document NZ's profiles require. Co-Authored-By: GPT-6.1 Sol Co-Authored-By: Claude Opus 5.5 --- changelog.d/nz-reference-intervals.added.md | 23 + docs/nz-calibration-targets.md | 40 + docs/nz-take-up-engine-plan.md | 9 +- .../microcosm/build/calibration_columns.py | 100 +- .../microcosm/build/nz/country_package.json | 5 + .../build/nz/reference_intervals.json | 3464 +++++++++++++++++ .../build/nz/reference_profiles.json | 13 +- .../microcosm/build/reference_intervals.py | 981 +++++ .../src/microcosm/build/reference_profiles.py | 215 +- .../src/microcosm/build/transport/compose.py | 10 +- .../build/transport/target_kernels.py | 148 +- .../shared/test_calibration_columns.py | 194 + .../shared/test_nz_reference_intervals.py | 340 ++ .../shared/test_nz_target_references.py | 17 +- .../shared/test_reference_intervals.py | 1129 ++++++ .../shared/test_reference_profiles.py | 344 +- .../test_spec_engine_country_bundles.py | 1 + .../shared/test_transport_composed_graph.py | 170 + .../shared/test_transport_target_kernels.py | 268 +- .../tests/golden/nz_country_spec.json | 8 +- .../microcosm_build/reference_intervals.py | 190 + tools/generate_nz_target_references.py | 203 +- 22 files changed, 7823 insertions(+), 49 deletions(-) create mode 100644 changelog.d/nz-reference-intervals.added.md create mode 100644 packages/microcosm-build/src/microcosm/build/nz/reference_intervals.json create mode 100644 packages/microcosm-build/src/microcosm/build/reference_intervals.py create mode 100644 packages/microcosm-build/tests/engine_free/shared/test_nz_reference_intervals.py create mode 100644 packages/microcosm-build/tests/engine_free/shared/test_reference_intervals.py create mode 100644 test_support/microcosm_build/reference_intervals.py diff --git a/changelog.d/nz-reference-intervals.added.md b/changelog.d/nz-reference-intervals.added.md new file mode 100644 index 0000000000..e1b822534c --- /dev/null +++ b/changelog.d/nz-reference-intervals.added.md @@ -0,0 +1,23 @@ +Add exact income-band intervals: a shared, declaration-driven interval module +with no default semantics (closure, nil band, negative-income policy, first lower +cut, entity, columns and diagnostics window are all declared per table), +integer-cent source cuts, exact rational scaling that never rounds an aged cut, +universe-aware classification that refuses unresolved records or invalid amounts +before any indicator exists, and fixed-weight boundary diagnostics weighted +through `resolve_weights(entity)`. + +`scale_money_and_intervals` now scales named interval tables with the same +selected factor as their money companions and binds each scaled table to its +source and factor. Interval outputs are their own calibration binding kind. +Preparation passes the effective interval document into column preparation and +records its digest, payload and input fingerprints; the problem kernel binds a +boundary diagnostics report without changing any target. Composition supplies +each variant its own effective interval companion. Declare the NZ national +(237 bands) and age (183 bands) taxable-income tables under Microcosm's declared +convention; the 474 IRD references stay parked. + +Require the first cut explicitly and refuse overlapping nil and left-closed +zero bands. Report below-first-cut mass and floored negative records, including +weighted counts and signed pre-floor amounts by destination. These diagnostics +describe fixed weights before fitting; reporting later weight changes on +unfitted records remains G8-r2c/G8-x work. diff --git a/docs/nz-calibration-targets.md b/docs/nz-calibration-targets.md index 7ed4c9b4f2..0c43e0adc0 100644 --- a/docs/nz-calibration-targets.md +++ b/docs/nz-calibration-targets.md @@ -98,6 +98,46 @@ All statistical spreadsheets on the un-gated `ird.govt.nz` CDN (HTTP 200 confirm Notes: **I1, I3, I4 are the load-bearing IRD targets.** IRD income tables are **individual-level** — joint/couple income unpublished. **FamilyBoost: no public statistics dataset confirmed** — only a modelled `Value_Type` in Treasury DistributionExplorer [UNCONFIRMED — re-check]. +### I1 taxable-income bands: a declared convention (G8-r2b, 2026-10-24) + +IRD publishes no band closure or cents semantics for the I1 tables. The +workbook introduces an "income between $0.01 and $100" band, and four label +pairs share an endpoint ($20,000, $50,000, $80,000 and $300,000: national rows +29/30, 59/60, 89/90 and 229/230; age rows 27/28, 57/58 and 87/88). Microcosm +therefore applies its own declared convention, not IRD's semantics (G8-r open +rulings 2, section 1): + +- t = max(0, x) in integer cents, with no dollar rounding; +- t == 0 is the nil band, including qualifying loss records; +- each positive band is `previous_upper < t <= upper`, and the open tail is + `t > its lower cut` (national top band over $1,000,000, age table over + $180,000); +- the ordered right-closed cuts give each shared endpoint to the lower band. + +`build/nz/reference_intervals.json` holds the two tables: the national table +(237 bands, rows 8–244, each naming its parked count and amount references) +and the age table (183 bands, rows 6–188, no references and no fit rows). +`tools/generate_nz_target_references.py` generates both from the row formulas +and checks every cut against the pinned feed's own band labels. Ageing scales +the national cuts exactly by the selected factor and keeps fractional cents: +under the central factor 1.117109, $100 becomes 11,171.09 cents and $1,000 +becomes 111,710.9 cents. Before matrix construction the calibration problem +reports, for every cut, the weighted mass within $1, and for every band the +fixed-weight change under the left-closed alternative. These diagnostics never +change a target, and publisher reclassification is unavailable, because the +published histogram does not identify mass exactly on a cut. + +The shared diagnostics also report records below a table's first lower cut, +including their weighted count and amount. Floored negative records retain +their separate count and signed pre-floor amount, grouped by destination. +These are fixed-weight, pre-fit measurements; reporting the later weight +changes on these unfitted records remains G8-r2c/G8-x work. + +The income and universe columns (`ird_taxable_income_proxy_cents`, +`ird_taxable_income_proxy_universe_status`) name the d1952 stand-in measure +rather than IRD's definition. No producer exists yet (G8-r2c), no active row +requests them, and all 474 IRD rows stay parked. + ## 3. MSD **Critical structural finding:** the **quarterly Benefit Fact Sheets are COUNT-ONLY** (every sheet opened; only hardship reports dollars). **Dollar-level data comes from the Annual Report** expenditure schedule (explicit CC BY 4.0). diff --git a/docs/nz-take-up-engine-plan.md b/docs/nz-take-up-engine-plan.md index 1569e61cf8..2e765dffff 100644 --- a/docs/nz-take-up-engine-plan.md +++ b/docs/nz-take-up-engine-plan.md @@ -129,9 +129,12 @@ tenure targets close to each region's published Census household total before the existing ERP bridge. Calibration variant V4 concentrates only the size residual onto one- and two-person households. The implementation preserves raw discrepancies and independent national diagnostics, and supplies validated size -columns before activation. G8-r2 reuses the same resolver for ageing profiles -and interval preparation; central ageing remains explicitly deferred and V1/V2 -ageing transformations remain unavailable until that work lands. +columns before activation. G8-r2 reuses the same resolver for ageing: each +low, central or high profile scales the IRD money references and, exactly, the +national taxable-income band cuts by its own feed-derived factor, and the +interval tables declare Microcosm's band convention (see +[the target inventory](nz-calibration-targets.md)). The 474 IRD references +stay parked until the B4 stand-in measure's producer lands (G8-r2c). - **Pre-calibration:** IRD wage and salary bands, MBIE bond rents by TA, Stats NZ household financial net worth, TA population, and the same MSD recipient facts as the calibration set (the take-up draws calibrate to them). diff --git a/packages/microcosm-build/src/microcosm/build/calibration_columns.py b/packages/microcosm-build/src/microcosm/build/calibration_columns.py index be3897d360..c7a986b21b 100644 --- a/packages/microcosm-build/src/microcosm/build/calibration_columns.py +++ b/packages/microcosm-build/src/microcosm/build/calibration_columns.py @@ -1,7 +1,8 @@ """Prepare simple calibration columns from an explicit, engine-free contract. Geography and canonical concepts must already be present. This helper creates -constants, validated roster counts and conjunctions of declared predicates; +constants, validated roster counts, conjunctions of declared predicates and +interval indicators classified by :mod:`microcosm.build.reference_intervals`; receipt assignment and statistical-unit construction belong to their own producers. """ @@ -15,15 +16,26 @@ import numpy as np import pandas as pd +from microcosm.build.reference_intervals import ( + interval_indicators, + validate_interval_document, +) + def prepare_calibration_columns( - tables: Mapping[str, pd.DataFrame], bindings: Mapping[str, Any] + tables: Mapping[str, pd.DataFrame], + bindings: Mapping[str, Any], + *, + intervals: Mapping[str, Any] | None = None, ) -> dict[str, pd.DataFrame]: """Return independent tables with the contract's prepared columns. All required inputs are validated before any column is produced. Existing prepared columns are recomputed, so a changed geography or age cannot retain stale indicators. Missing/unknown inputs never become all-false categories. + An interval output names its entity, an effective table of ``intervals`` + and one of its bands; every record of each named table is validated, and + any refusal raises, before an output exists. """ if bindings.get("schema_version") != 1: raise ValueError("Calibration bindings require schema_version 1.") @@ -48,6 +60,7 @@ def prepare_calibration_columns( raise ValueError(f"Calibration input {entity}.{name} needs a domain.") _validate_input(tables[entity][name], domain, f"{entity}.{name}") + interval_columns = _interval_columns(tables, columns, required, intervals) out = {entity: table.copy(deep=True) for entity, table in tables.items()} derived = _prepare_roster_counts(out, bindings.get("roster_counts", ()), required) prepared: dict[str, dict[str, object]] = {} @@ -69,6 +82,9 @@ def prepare_calibration_columns( f"Calibration output {entity}.{name} is repeated or structural." ) seen.add(key) + if "interval" in entry: + prepared.setdefault(entity, {})[name] = interval_columns[key] + continue if "constant" in entry: value = entry["constant"] if "predicates" in entry or not _finite_number(value): @@ -115,6 +131,81 @@ def prepare_calibration_columns( return out +def _interval_columns( + tables: Mapping[str, pd.DataFrame], + columns: object, + required: Mapping[str, Any], + intervals: Mapping[str, Any] | None, +) -> dict[tuple[str, str], np.ndarray]: + """Classify every named table once; refuse before any output is produced.""" + entries = [ + entry for entry in columns if isinstance(entry, Mapping) and "interval" in entry + ] + if not entries: + return {} + if intervals is None: + raise ValueError("Interval outputs need the effective interval document.") + document = validate_interval_document(intervals) + requested: dict[str, list[tuple[str, str, str]]] = {} + for entry in entries: + entity, name, binding = ( + entry.get("entity"), + entry.get("name"), + entry["interval"], + ) + if set(entry) != {"entity", "name", "interval"}: + raise ValueError( + f"Interval output {entity}.{name} declares only its entity, name " + "and interval." + ) + if not isinstance(binding, Mapping) or set(binding) != {"table", "band"}: + raise ValueError("An interval output names exactly its table and band.") + table = document.get(binding["table"]) + if table is None or binding["band"] not in table.band_ids: + raise ValueError( + f"Interval output {entity}.{name} names an undeclared table or band." + ) + if entity not in required or entity != table.entity: + raise ValueError( + f"Interval output {entity}.{name} must name its table's entity." + ) + if name != table.indicators[table.band_ids.index(binding["band"])]: + raise ValueError( + f"Interval output {entity}.{name} must be its band's declared indicator." + ) + if ( + table.declaration["source_period"] != table.declaration["target_period"] + and not table.scaled + ): + raise ValueError( + f"Interval table {table.id!r} has source-period cuts; scale it to its " + "target period before preparing indicators." + ) + requested.setdefault(table.id, []).append((entity, name, binding["band"])) + result: dict[tuple[str, str], np.ndarray] = {} + computed = {} + for table_id in requested: + table = document[table_id] + data = tables.get(table.entity) + if data is None or any( + column not in data + for column in (table.income_column, table.universe_column) + ): + raise ValueError( + f"Interval table {table_id!r} needs {table.entity}." + f"{table.income_column} and {table.entity}.{table.universe_column}." + ) + computed[table_id] = interval_indicators( + table, + data[table.income_column].tolist(), + data[table.universe_column].tolist(), + ) + for table_id, outputs in requested.items(): + for entity, name, _ in outputs: + result[entity, name] = computed[table_id][name] + return result + + def _finite_number(value: object) -> bool: return ( isinstance(value, Real) and not isinstance(value, bool) and math.isfinite(value) @@ -196,6 +287,11 @@ def _validate_input(values: pd.Series, domain: Mapping[str, Any], name: str) -> for value in values.tolist(): _identity(value) valid = True + elif domain.get("kind") == "signed_integer": + valid = all( + isinstance(value, Integral) and not isinstance(value, (bool, np.bool_)) + for value in values.tolist() + ) elif domain.get("kind") == "nonnegative_integer": valid = all( _finite_number(value) and value >= 0 and value == int(value) diff --git a/packages/microcosm-build/src/microcosm/build/nz/country_package.json b/packages/microcosm-build/src/microcosm/build/nz/country_package.json index 7ada3b19fb..2249271796 100644 --- a/packages/microcosm-build/src/microcosm/build/nz/country_package.json +++ b/packages/microcosm-build/src/microcosm/build/nz/country_package.json @@ -102,6 +102,11 @@ "kind": "legacy_json", "schema_id": "legacy_json" }, + { + "path": "reference_intervals.json", + "kind": "legacy_json", + "schema_id": "legacy_json" + }, { "path": "reference_inventory.json", "kind": "legacy_json", diff --git a/packages/microcosm-build/src/microcosm/build/nz/reference_intervals.json b/packages/microcosm-build/src/microcosm/build/nz/reference_intervals.json new file mode 100644 index 0000000000..991a9b4984 --- /dev/null +++ b/packages/microcosm-build/src/microcosm/build/nz/reference_intervals.json @@ -0,0 +1,3464 @@ +{ + "schema_version": 1, + "country": "nz", + "description": "Generated by tools/generate_nz_target_references.py from the row formulas and checked against the pinned feed's own band labels.", + "disclosure": "IRD publishes no band closure or cents semantics for these tables. The cuts, closure, nil band and integer-cent arithmetic are Microcosm's declared convention (G8-r open rulings 2, section 1, 23 October 2026), not IRD's own semantics: t = max(0, x) in integer cents with no dollar rounding; t == 0 is the nil band, including qualifying loss records; each positive band is previous_upper < t <= upper; and the open tail is t > its lower cut. Four published label pairs share an endpoint ($20,000, $50,000, $80,000 and $300,000: national rows 29/30, 59/60, 89/90 and 229/230; age rows 27/28, 57/58 and 87/88); the ordered right-closed cuts assign each shared endpoint to the lower band. Ageing scales every finite cut exactly by the selected factor and keeps fractional cents. The income and universe columns name the d1952 stand-in measure rather than IRD's definition; no producer exists yet (G8-r2c) and no active row requests them. Boundary diagnostics report weighted mass within $1 of every cut and the fixed-weight change under the left-closed alternative; publisher reclassification is unavailable.", + "tables": [ + { + "id": "ird_taxable_income_national_ty2024", + "entity": "person", + "currency": "NZD", + "input_unit": "integer_cent", + "income_column": "ird_taxable_income_proxy_cents", + "universe_column": "ird_taxable_income_proxy_universe_status", + "source_period": 2024, + "target_period": 2027, + "positive_band_closure": "right", + "nil_band": "present", + "negative_income_policy": "floor_zero", + "first_lower_cut_cents": 0, + "source_locator": "Income tables 2001-24!BE8:BE244", + "diagnostics": { + "boundary_window_cents": 100, + "alternative_positive_band_closure": "left" + }, + "bands": [ + { + "id": "row_8", + "kind": "nil", + "indicator": "taxable_income_band_row_8_indicator", + "source_label": "nil", + "count_reference": "ird_taxable_income_people_by_band__row_8", + "amount_reference": "ird_taxable_income_total_by_band__row_8" + }, + { + "id": "row_9", + "kind": "positive", + "upper_cent": 10000, + "indicator": "taxable_income_band_row_9_indicator", + "source_label": "$0.01 - $100", + "count_reference": "ird_taxable_income_people_by_band__row_9", + "amount_reference": "ird_taxable_income_total_by_band__row_9" + }, + { + "id": "row_10", + "kind": "positive", + "upper_cent": 100000, + "indicator": "taxable_income_band_row_10_indicator", + "source_label": "$101 - $1,000", + "count_reference": "ird_taxable_income_people_by_band__row_10", + "amount_reference": "ird_taxable_income_total_by_band__row_10" + }, + { + "id": "row_11", + "kind": "positive", + "upper_cent": 200000, + "indicator": "taxable_income_band_row_11_indicator", + "source_label": "$1,001 - $2,000", + "count_reference": "ird_taxable_income_people_by_band__row_11", + "amount_reference": "ird_taxable_income_total_by_band__row_11" + }, + { + "id": "row_12", + "kind": "positive", + "upper_cent": 300000, + "indicator": "taxable_income_band_row_12_indicator", + "source_label": "$2,001 - $3,000", + "count_reference": "ird_taxable_income_people_by_band__row_12", + "amount_reference": "ird_taxable_income_total_by_band__row_12" + }, + { + "id": "row_13", + "kind": "positive", + "upper_cent": 400000, + "indicator": "taxable_income_band_row_13_indicator", + "source_label": "$3,001 - $4,000", + "count_reference": "ird_taxable_income_people_by_band__row_13", + "amount_reference": "ird_taxable_income_total_by_band__row_13" + }, + { + "id": "row_14", + "kind": "positive", + "upper_cent": 500000, + "indicator": "taxable_income_band_row_14_indicator", + "source_label": "$4,001 - $5,000", + "count_reference": "ird_taxable_income_people_by_band__row_14", + "amount_reference": "ird_taxable_income_total_by_band__row_14" + }, + { + "id": "row_15", + "kind": "positive", + "upper_cent": 600000, + "indicator": "taxable_income_band_row_15_indicator", + "source_label": "$5,001 - $6,000", + "count_reference": "ird_taxable_income_people_by_band__row_15", + "amount_reference": "ird_taxable_income_total_by_band__row_15" + }, + { + "id": "row_16", + "kind": "positive", + "upper_cent": 700000, + "indicator": "taxable_income_band_row_16_indicator", + "source_label": "$6,001 - $7,000", + "count_reference": "ird_taxable_income_people_by_band__row_16", + "amount_reference": "ird_taxable_income_total_by_band__row_16" + }, + { + "id": "row_17", + "kind": "positive", + "upper_cent": 800000, + "indicator": "taxable_income_band_row_17_indicator", + "source_label": "$7,001 - $8,000", + "count_reference": "ird_taxable_income_people_by_band__row_17", + "amount_reference": "ird_taxable_income_total_by_band__row_17" + }, + { + "id": "row_18", + "kind": "positive", + "upper_cent": 900000, + "indicator": "taxable_income_band_row_18_indicator", + "source_label": "$8,001 - $9,000", + "count_reference": "ird_taxable_income_people_by_band__row_18", + "amount_reference": "ird_taxable_income_total_by_band__row_18" + }, + { + "id": "row_19", + "kind": "positive", + "upper_cent": 1000000, + "indicator": "taxable_income_band_row_19_indicator", + "source_label": "$9,001 - $10,000", + "count_reference": "ird_taxable_income_people_by_band__row_19", + "amount_reference": "ird_taxable_income_total_by_band__row_19" + }, + { + "id": "row_20", + "kind": "positive", + "upper_cent": 1100000, + "indicator": "taxable_income_band_row_20_indicator", + "source_label": "$10,001 - $11,000", + "count_reference": "ird_taxable_income_people_by_band__row_20", + "amount_reference": "ird_taxable_income_total_by_band__row_20" + }, + { + "id": "row_21", + "kind": "positive", + "upper_cent": 1200000, + "indicator": "taxable_income_band_row_21_indicator", + "source_label": "$11,001 - $12,000", + "count_reference": "ird_taxable_income_people_by_band__row_21", + "amount_reference": "ird_taxable_income_total_by_band__row_21" + }, + { + "id": "row_22", + "kind": "positive", + "upper_cent": 1300000, + "indicator": "taxable_income_band_row_22_indicator", + "source_label": "$12,001 - $13,000", + "count_reference": "ird_taxable_income_people_by_band__row_22", + "amount_reference": "ird_taxable_income_total_by_band__row_22" + }, + { + "id": "row_23", + "kind": "positive", + "upper_cent": 1400000, + "indicator": "taxable_income_band_row_23_indicator", + "source_label": "$13,001 - $14,000", + "count_reference": "ird_taxable_income_people_by_band__row_23", + "amount_reference": "ird_taxable_income_total_by_band__row_23" + }, + { + "id": "row_24", + "kind": "positive", + "upper_cent": 1500000, + "indicator": "taxable_income_band_row_24_indicator", + "source_label": "$14,001 - $15,000", + "count_reference": "ird_taxable_income_people_by_band__row_24", + "amount_reference": "ird_taxable_income_total_by_band__row_24" + }, + { + "id": "row_25", + "kind": "positive", + "upper_cent": 1600000, + "indicator": "taxable_income_band_row_25_indicator", + "source_label": "$15,001 - $16,000", + "count_reference": "ird_taxable_income_people_by_band__row_25", + "amount_reference": "ird_taxable_income_total_by_band__row_25" + }, + { + "id": "row_26", + "kind": "positive", + "upper_cent": 1700000, + "indicator": "taxable_income_band_row_26_indicator", + "source_label": "$16,001 - $17,000", + "count_reference": "ird_taxable_income_people_by_band__row_26", + "amount_reference": "ird_taxable_income_total_by_band__row_26" + }, + { + "id": "row_27", + "kind": "positive", + "upper_cent": 1800000, + "indicator": "taxable_income_band_row_27_indicator", + "source_label": "$17,001 - $18,000", + "count_reference": "ird_taxable_income_people_by_band__row_27", + "amount_reference": "ird_taxable_income_total_by_band__row_27" + }, + { + "id": "row_28", + "kind": "positive", + "upper_cent": 1900000, + "indicator": "taxable_income_band_row_28_indicator", + "source_label": "$18,001 - $19,000", + "count_reference": "ird_taxable_income_people_by_band__row_28", + "amount_reference": "ird_taxable_income_total_by_band__row_28" + }, + { + "id": "row_29", + "kind": "positive", + "upper_cent": 2000000, + "indicator": "taxable_income_band_row_29_indicator", + "source_label": "$19,001 - $20,000", + "count_reference": "ird_taxable_income_people_by_band__row_29", + "amount_reference": "ird_taxable_income_total_by_band__row_29" + }, + { + "id": "row_30", + "kind": "positive", + "upper_cent": 2100000, + "indicator": "taxable_income_band_row_30_indicator", + "source_label": "$20,000 - $21,000", + "count_reference": "ird_taxable_income_people_by_band__row_30", + "amount_reference": "ird_taxable_income_total_by_band__row_30" + }, + { + "id": "row_31", + "kind": "positive", + "upper_cent": 2200000, + "indicator": "taxable_income_band_row_31_indicator", + "source_label": "$21,001 - $22,000", + "count_reference": "ird_taxable_income_people_by_band__row_31", + "amount_reference": "ird_taxable_income_total_by_band__row_31" + }, + { + "id": "row_32", + "kind": "positive", + "upper_cent": 2300000, + "indicator": "taxable_income_band_row_32_indicator", + "source_label": "$22,001 - $23,000", + "count_reference": "ird_taxable_income_people_by_band__row_32", + "amount_reference": "ird_taxable_income_total_by_band__row_32" + }, + { + "id": "row_33", + "kind": "positive", + "upper_cent": 2400000, + "indicator": "taxable_income_band_row_33_indicator", + "source_label": "$23,001 - $24,000", + "count_reference": "ird_taxable_income_people_by_band__row_33", + "amount_reference": "ird_taxable_income_total_by_band__row_33" + }, + { + "id": "row_34", + "kind": "positive", + "upper_cent": 2500000, + "indicator": "taxable_income_band_row_34_indicator", + "source_label": "$24,001 - $25,000", + "count_reference": "ird_taxable_income_people_by_band__row_34", + "amount_reference": "ird_taxable_income_total_by_band__row_34" + }, + { + "id": "row_35", + "kind": "positive", + "upper_cent": 2600000, + "indicator": "taxable_income_band_row_35_indicator", + "source_label": "$25,001 - $26,000", + "count_reference": "ird_taxable_income_people_by_band__row_35", + "amount_reference": "ird_taxable_income_total_by_band__row_35" + }, + { + "id": "row_36", + "kind": "positive", + "upper_cent": 2700000, + "indicator": "taxable_income_band_row_36_indicator", + "source_label": "$26,001 - $27,000", + "count_reference": "ird_taxable_income_people_by_band__row_36", + "amount_reference": "ird_taxable_income_total_by_band__row_36" + }, + { + "id": "row_37", + "kind": "positive", + "upper_cent": 2800000, + "indicator": "taxable_income_band_row_37_indicator", + "source_label": "$27,001 - $28,000", + "count_reference": "ird_taxable_income_people_by_band__row_37", + "amount_reference": "ird_taxable_income_total_by_band__row_37" + }, + { + "id": "row_38", + "kind": "positive", + "upper_cent": 2900000, + "indicator": "taxable_income_band_row_38_indicator", + "source_label": "$28,001 - $29,000", + "count_reference": "ird_taxable_income_people_by_band__row_38", + "amount_reference": "ird_taxable_income_total_by_band__row_38" + }, + { + "id": "row_39", + "kind": "positive", + "upper_cent": 3000000, + "indicator": "taxable_income_band_row_39_indicator", + "source_label": "$29,001 - $30,000", + "count_reference": "ird_taxable_income_people_by_band__row_39", + "amount_reference": "ird_taxable_income_total_by_band__row_39" + }, + { + "id": "row_40", + "kind": "positive", + "upper_cent": 3100000, + "indicator": "taxable_income_band_row_40_indicator", + "source_label": "$30,001 - $31,000", + "count_reference": "ird_taxable_income_people_by_band__row_40", + "amount_reference": "ird_taxable_income_total_by_band__row_40" + }, + { + "id": "row_41", + "kind": "positive", + "upper_cent": 3200000, + "indicator": "taxable_income_band_row_41_indicator", + "source_label": "$31,001 - $32,000", + "count_reference": "ird_taxable_income_people_by_band__row_41", + "amount_reference": "ird_taxable_income_total_by_band__row_41" + }, + { + "id": "row_42", + "kind": "positive", + "upper_cent": 3300000, + "indicator": "taxable_income_band_row_42_indicator", + "source_label": "$32,001 - $33,000", + "count_reference": "ird_taxable_income_people_by_band__row_42", + "amount_reference": "ird_taxable_income_total_by_band__row_42" + }, + { + "id": "row_43", + "kind": "positive", + "upper_cent": 3400000, + "indicator": "taxable_income_band_row_43_indicator", + "source_label": "$33,001 - $34,000", + "count_reference": "ird_taxable_income_people_by_band__row_43", + "amount_reference": "ird_taxable_income_total_by_band__row_43" + }, + { + "id": "row_44", + "kind": "positive", + "upper_cent": 3500000, + "indicator": "taxable_income_band_row_44_indicator", + "source_label": "$34,001 - $35,000", + "count_reference": "ird_taxable_income_people_by_band__row_44", + "amount_reference": "ird_taxable_income_total_by_band__row_44" + }, + { + "id": "row_45", + "kind": "positive", + "upper_cent": 3600000, + "indicator": "taxable_income_band_row_45_indicator", + "source_label": "$35,001 - $36,000", + "count_reference": "ird_taxable_income_people_by_band__row_45", + "amount_reference": "ird_taxable_income_total_by_band__row_45" + }, + { + "id": "row_46", + "kind": "positive", + "upper_cent": 3700000, + "indicator": "taxable_income_band_row_46_indicator", + "source_label": "$36,001 - $37,000", + "count_reference": "ird_taxable_income_people_by_band__row_46", + "amount_reference": "ird_taxable_income_total_by_band__row_46" + }, + { + "id": "row_47", + "kind": "positive", + "upper_cent": 3800000, + "indicator": "taxable_income_band_row_47_indicator", + "source_label": "$37,001 - $38,000", + "count_reference": "ird_taxable_income_people_by_band__row_47", + "amount_reference": "ird_taxable_income_total_by_band__row_47" + }, + { + "id": "row_48", + "kind": "positive", + "upper_cent": 3900000, + "indicator": "taxable_income_band_row_48_indicator", + "source_label": "$38,001 - $39,000", + "count_reference": "ird_taxable_income_people_by_band__row_48", + "amount_reference": "ird_taxable_income_total_by_band__row_48" + }, + { + "id": "row_49", + "kind": "positive", + "upper_cent": 4000000, + "indicator": "taxable_income_band_row_49_indicator", + "source_label": "$39,001 - $40,000", + "count_reference": "ird_taxable_income_people_by_band__row_49", + "amount_reference": "ird_taxable_income_total_by_band__row_49" + }, + { + "id": "row_50", + "kind": "positive", + "upper_cent": 4100000, + "indicator": "taxable_income_band_row_50_indicator", + "source_label": "$40,001 - $41,000", + "count_reference": "ird_taxable_income_people_by_band__row_50", + "amount_reference": "ird_taxable_income_total_by_band__row_50" + }, + { + "id": "row_51", + "kind": "positive", + "upper_cent": 4200000, + "indicator": "taxable_income_band_row_51_indicator", + "source_label": "$41,001 - $42,000", + "count_reference": "ird_taxable_income_people_by_band__row_51", + "amount_reference": "ird_taxable_income_total_by_band__row_51" + }, + { + "id": "row_52", + "kind": "positive", + "upper_cent": 4300000, + "indicator": "taxable_income_band_row_52_indicator", + "source_label": "$42,001 - $43,000", + "count_reference": "ird_taxable_income_people_by_band__row_52", + "amount_reference": "ird_taxable_income_total_by_band__row_52" + }, + { + "id": "row_53", + "kind": "positive", + "upper_cent": 4400000, + "indicator": "taxable_income_band_row_53_indicator", + "source_label": "$43,001 - $44,000", + "count_reference": "ird_taxable_income_people_by_band__row_53", + "amount_reference": "ird_taxable_income_total_by_band__row_53" + }, + { + "id": "row_54", + "kind": "positive", + "upper_cent": 4500000, + "indicator": "taxable_income_band_row_54_indicator", + "source_label": "$44,001 - $45,000", + "count_reference": "ird_taxable_income_people_by_band__row_54", + "amount_reference": "ird_taxable_income_total_by_band__row_54" + }, + { + "id": "row_55", + "kind": "positive", + "upper_cent": 4600000, + "indicator": "taxable_income_band_row_55_indicator", + "source_label": "$45,001 - $46,000", + "count_reference": "ird_taxable_income_people_by_band__row_55", + "amount_reference": "ird_taxable_income_total_by_band__row_55" + }, + { + "id": "row_56", + "kind": "positive", + "upper_cent": 4700000, + "indicator": "taxable_income_band_row_56_indicator", + "source_label": "$46,001 - $47,000", + "count_reference": "ird_taxable_income_people_by_band__row_56", + "amount_reference": "ird_taxable_income_total_by_band__row_56" + }, + { + "id": "row_57", + "kind": "positive", + "upper_cent": 4800000, + "indicator": "taxable_income_band_row_57_indicator", + "source_label": "$47,001 - $48,000", + "count_reference": "ird_taxable_income_people_by_band__row_57", + "amount_reference": "ird_taxable_income_total_by_band__row_57" + }, + { + "id": "row_58", + "kind": "positive", + "upper_cent": 4900000, + "indicator": "taxable_income_band_row_58_indicator", + "source_label": "$48,001 - $49,000", + "count_reference": "ird_taxable_income_people_by_band__row_58", + "amount_reference": "ird_taxable_income_total_by_band__row_58" + }, + { + "id": "row_59", + "kind": "positive", + "upper_cent": 5000000, + "indicator": "taxable_income_band_row_59_indicator", + "source_label": "$49,001 - $50,000", + "count_reference": "ird_taxable_income_people_by_band__row_59", + "amount_reference": "ird_taxable_income_total_by_band__row_59" + }, + { + "id": "row_60", + "kind": "positive", + "upper_cent": 5100000, + "indicator": "taxable_income_band_row_60_indicator", + "source_label": "$50,000 - $51,000", + "count_reference": "ird_taxable_income_people_by_band__row_60", + "amount_reference": "ird_taxable_income_total_by_band__row_60" + }, + { + "id": "row_61", + "kind": "positive", + "upper_cent": 5200000, + "indicator": "taxable_income_band_row_61_indicator", + "source_label": "$51,001 - $52,000", + "count_reference": "ird_taxable_income_people_by_band__row_61", + "amount_reference": "ird_taxable_income_total_by_band__row_61" + }, + { + "id": "row_62", + "kind": "positive", + "upper_cent": 5300000, + "indicator": "taxable_income_band_row_62_indicator", + "source_label": "$52,001 - $53,000", + "count_reference": "ird_taxable_income_people_by_band__row_62", + "amount_reference": "ird_taxable_income_total_by_band__row_62" + }, + { + "id": "row_63", + "kind": "positive", + "upper_cent": 5400000, + "indicator": "taxable_income_band_row_63_indicator", + "source_label": "$53,001 - $54,000", + "count_reference": "ird_taxable_income_people_by_band__row_63", + "amount_reference": "ird_taxable_income_total_by_band__row_63" + }, + { + "id": "row_64", + "kind": "positive", + "upper_cent": 5500000, + "indicator": "taxable_income_band_row_64_indicator", + "source_label": "$54,001 - $55,000", + "count_reference": "ird_taxable_income_people_by_band__row_64", + "amount_reference": "ird_taxable_income_total_by_band__row_64" + }, + { + "id": "row_65", + "kind": "positive", + "upper_cent": 5600000, + "indicator": "taxable_income_band_row_65_indicator", + "source_label": "$55,001 - $56,000", + "count_reference": "ird_taxable_income_people_by_band__row_65", + "amount_reference": "ird_taxable_income_total_by_band__row_65" + }, + { + "id": "row_66", + "kind": "positive", + "upper_cent": 5700000, + "indicator": "taxable_income_band_row_66_indicator", + "source_label": "$56,001 - $57,000", + "count_reference": "ird_taxable_income_people_by_band__row_66", + "amount_reference": "ird_taxable_income_total_by_band__row_66" + }, + { + "id": "row_67", + "kind": "positive", + "upper_cent": 5800000, + "indicator": "taxable_income_band_row_67_indicator", + "source_label": "$57,001 - $58,000", + "count_reference": "ird_taxable_income_people_by_band__row_67", + "amount_reference": "ird_taxable_income_total_by_band__row_67" + }, + { + "id": "row_68", + "kind": "positive", + "upper_cent": 5900000, + "indicator": "taxable_income_band_row_68_indicator", + "source_label": "$58,001 - $59,000", + "count_reference": "ird_taxable_income_people_by_band__row_68", + "amount_reference": "ird_taxable_income_total_by_band__row_68" + }, + { + "id": "row_69", + "kind": "positive", + "upper_cent": 6000000, + "indicator": "taxable_income_band_row_69_indicator", + "source_label": "$59,001 - $60,000", + "count_reference": "ird_taxable_income_people_by_band__row_69", + "amount_reference": "ird_taxable_income_total_by_band__row_69" + }, + { + "id": "row_70", + "kind": "positive", + "upper_cent": 6100000, + "indicator": "taxable_income_band_row_70_indicator", + "source_label": "$60,001 - $61,000", + "count_reference": "ird_taxable_income_people_by_band__row_70", + "amount_reference": "ird_taxable_income_total_by_band__row_70" + }, + { + "id": "row_71", + "kind": "positive", + "upper_cent": 6200000, + "indicator": "taxable_income_band_row_71_indicator", + "source_label": "$61,001 - $62,000", + "count_reference": "ird_taxable_income_people_by_band__row_71", + "amount_reference": "ird_taxable_income_total_by_band__row_71" + }, + { + "id": "row_72", + "kind": "positive", + "upper_cent": 6300000, + "indicator": "taxable_income_band_row_72_indicator", + "source_label": "$62,001 - $63,000", + "count_reference": "ird_taxable_income_people_by_band__row_72", + "amount_reference": "ird_taxable_income_total_by_band__row_72" + }, + { + "id": "row_73", + "kind": "positive", + "upper_cent": 6400000, + "indicator": "taxable_income_band_row_73_indicator", + "source_label": "$63,001 - $64,000", + "count_reference": "ird_taxable_income_people_by_band__row_73", + "amount_reference": "ird_taxable_income_total_by_band__row_73" + }, + { + "id": "row_74", + "kind": "positive", + "upper_cent": 6500000, + "indicator": "taxable_income_band_row_74_indicator", + "source_label": "$64,001 - $65,000", + "count_reference": "ird_taxable_income_people_by_band__row_74", + "amount_reference": "ird_taxable_income_total_by_band__row_74" + }, + { + "id": "row_75", + "kind": "positive", + "upper_cent": 6600000, + "indicator": "taxable_income_band_row_75_indicator", + "source_label": "$65,001 - $66,000", + "count_reference": "ird_taxable_income_people_by_band__row_75", + "amount_reference": "ird_taxable_income_total_by_band__row_75" + }, + { + "id": "row_76", + "kind": "positive", + "upper_cent": 6700000, + "indicator": "taxable_income_band_row_76_indicator", + "source_label": "$66,001 - $67,000", + "count_reference": "ird_taxable_income_people_by_band__row_76", + "amount_reference": "ird_taxable_income_total_by_band__row_76" + }, + { + "id": "row_77", + "kind": "positive", + "upper_cent": 6800000, + "indicator": "taxable_income_band_row_77_indicator", + "source_label": "$67,001 - $68,000", + "count_reference": "ird_taxable_income_people_by_band__row_77", + "amount_reference": "ird_taxable_income_total_by_band__row_77" + }, + { + "id": "row_78", + "kind": "positive", + "upper_cent": 6900000, + "indicator": "taxable_income_band_row_78_indicator", + "source_label": "$68,001 - $69,000", + "count_reference": "ird_taxable_income_people_by_band__row_78", + "amount_reference": "ird_taxable_income_total_by_band__row_78" + }, + { + "id": "row_79", + "kind": "positive", + "upper_cent": 7000000, + "indicator": "taxable_income_band_row_79_indicator", + "source_label": "$69,001 - $70,000", + "count_reference": "ird_taxable_income_people_by_band__row_79", + "amount_reference": "ird_taxable_income_total_by_band__row_79" + }, + { + "id": "row_80", + "kind": "positive", + "upper_cent": 7100000, + "indicator": "taxable_income_band_row_80_indicator", + "source_label": "$70,001 - $71,000", + "count_reference": "ird_taxable_income_people_by_band__row_80", + "amount_reference": "ird_taxable_income_total_by_band__row_80" + }, + { + "id": "row_81", + "kind": "positive", + "upper_cent": 7200000, + "indicator": "taxable_income_band_row_81_indicator", + "source_label": "$71,001 - $72,000", + "count_reference": "ird_taxable_income_people_by_band__row_81", + "amount_reference": "ird_taxable_income_total_by_band__row_81" + }, + { + "id": "row_82", + "kind": "positive", + "upper_cent": 7300000, + "indicator": "taxable_income_band_row_82_indicator", + "source_label": "$72,001 - $73,000", + "count_reference": "ird_taxable_income_people_by_band__row_82", + "amount_reference": "ird_taxable_income_total_by_band__row_82" + }, + { + "id": "row_83", + "kind": "positive", + "upper_cent": 7400000, + "indicator": "taxable_income_band_row_83_indicator", + "source_label": "$73,001 - $74,000", + "count_reference": "ird_taxable_income_people_by_band__row_83", + "amount_reference": "ird_taxable_income_total_by_band__row_83" + }, + { + "id": "row_84", + "kind": "positive", + "upper_cent": 7500000, + "indicator": "taxable_income_band_row_84_indicator", + "source_label": "$74,001 - $75,000", + "count_reference": "ird_taxable_income_people_by_band__row_84", + "amount_reference": "ird_taxable_income_total_by_band__row_84" + }, + { + "id": "row_85", + "kind": "positive", + "upper_cent": 7600000, + "indicator": "taxable_income_band_row_85_indicator", + "source_label": "$75,001 - $76,000", + "count_reference": "ird_taxable_income_people_by_band__row_85", + "amount_reference": "ird_taxable_income_total_by_band__row_85" + }, + { + "id": "row_86", + "kind": "positive", + "upper_cent": 7700000, + "indicator": "taxable_income_band_row_86_indicator", + "source_label": "$76,001 - $77,000", + "count_reference": "ird_taxable_income_people_by_band__row_86", + "amount_reference": "ird_taxable_income_total_by_band__row_86" + }, + { + "id": "row_87", + "kind": "positive", + "upper_cent": 7800000, + "indicator": "taxable_income_band_row_87_indicator", + "source_label": "$77,001 - $78,000", + "count_reference": "ird_taxable_income_people_by_band__row_87", + "amount_reference": "ird_taxable_income_total_by_band__row_87" + }, + { + "id": "row_88", + "kind": "positive", + "upper_cent": 7900000, + "indicator": "taxable_income_band_row_88_indicator", + "source_label": "$78,001 - $79,000", + "count_reference": "ird_taxable_income_people_by_band__row_88", + "amount_reference": "ird_taxable_income_total_by_band__row_88" + }, + { + "id": "row_89", + "kind": "positive", + "upper_cent": 8000000, + "indicator": "taxable_income_band_row_89_indicator", + "source_label": "$79,001 - $80,000", + "count_reference": "ird_taxable_income_people_by_band__row_89", + "amount_reference": "ird_taxable_income_total_by_band__row_89" + }, + { + "id": "row_90", + "kind": "positive", + "upper_cent": 8100000, + "indicator": "taxable_income_band_row_90_indicator", + "source_label": "$80,000 - $81,000", + "count_reference": "ird_taxable_income_people_by_band__row_90", + "amount_reference": "ird_taxable_income_total_by_band__row_90" + }, + { + "id": "row_91", + "kind": "positive", + "upper_cent": 8200000, + "indicator": "taxable_income_band_row_91_indicator", + "source_label": "$81,001 - $82,000", + "count_reference": "ird_taxable_income_people_by_band__row_91", + "amount_reference": "ird_taxable_income_total_by_band__row_91" + }, + { + "id": "row_92", + "kind": "positive", + "upper_cent": 8300000, + "indicator": "taxable_income_band_row_92_indicator", + "source_label": "$82,001 - $83,000", + "count_reference": "ird_taxable_income_people_by_band__row_92", + "amount_reference": "ird_taxable_income_total_by_band__row_92" + }, + { + "id": "row_93", + "kind": "positive", + "upper_cent": 8400000, + "indicator": "taxable_income_band_row_93_indicator", + "source_label": "$83,001 - $84,000", + "count_reference": "ird_taxable_income_people_by_band__row_93", + "amount_reference": "ird_taxable_income_total_by_band__row_93" + }, + { + "id": "row_94", + "kind": "positive", + "upper_cent": 8500000, + "indicator": "taxable_income_band_row_94_indicator", + "source_label": "$84,001 - $85,000", + "count_reference": "ird_taxable_income_people_by_band__row_94", + "amount_reference": "ird_taxable_income_total_by_band__row_94" + }, + { + "id": "row_95", + "kind": "positive", + "upper_cent": 8600000, + "indicator": "taxable_income_band_row_95_indicator", + "source_label": "$85,001 - $86,000", + "count_reference": "ird_taxable_income_people_by_band__row_95", + "amount_reference": "ird_taxable_income_total_by_band__row_95" + }, + { + "id": "row_96", + "kind": "positive", + "upper_cent": 8700000, + "indicator": "taxable_income_band_row_96_indicator", + "source_label": "$86,001 - $87,000", + "count_reference": "ird_taxable_income_people_by_band__row_96", + "amount_reference": "ird_taxable_income_total_by_band__row_96" + }, + { + "id": "row_97", + "kind": "positive", + "upper_cent": 8800000, + "indicator": "taxable_income_band_row_97_indicator", + "source_label": "$87,001 - $88,000", + "count_reference": "ird_taxable_income_people_by_band__row_97", + "amount_reference": "ird_taxable_income_total_by_band__row_97" + }, + { + "id": "row_98", + "kind": "positive", + "upper_cent": 8900000, + "indicator": "taxable_income_band_row_98_indicator", + "source_label": "$88,001 - $89,000", + "count_reference": "ird_taxable_income_people_by_band__row_98", + "amount_reference": "ird_taxable_income_total_by_band__row_98" + }, + { + "id": "row_99", + "kind": "positive", + "upper_cent": 9000000, + "indicator": "taxable_income_band_row_99_indicator", + "source_label": "$89,001 - $90,000", + "count_reference": "ird_taxable_income_people_by_band__row_99", + "amount_reference": "ird_taxable_income_total_by_band__row_99" + }, + { + "id": "row_100", + "kind": "positive", + "upper_cent": 9100000, + "indicator": "taxable_income_band_row_100_indicator", + "source_label": "$90,001 - $91,000", + "count_reference": "ird_taxable_income_people_by_band__row_100", + "amount_reference": "ird_taxable_income_total_by_band__row_100" + }, + { + "id": "row_101", + "kind": "positive", + "upper_cent": 9200000, + "indicator": "taxable_income_band_row_101_indicator", + "source_label": "$91,001 - $92,000", + "count_reference": "ird_taxable_income_people_by_band__row_101", + "amount_reference": "ird_taxable_income_total_by_band__row_101" + }, + { + "id": "row_102", + "kind": "positive", + "upper_cent": 9300000, + "indicator": "taxable_income_band_row_102_indicator", + "source_label": "$92,001 - $93,000", + "count_reference": "ird_taxable_income_people_by_band__row_102", + "amount_reference": "ird_taxable_income_total_by_band__row_102" + }, + { + "id": "row_103", + "kind": "positive", + "upper_cent": 9400000, + "indicator": "taxable_income_band_row_103_indicator", + "source_label": "$93,001 - $94,000", + "count_reference": "ird_taxable_income_people_by_band__row_103", + "amount_reference": "ird_taxable_income_total_by_band__row_103" + }, + { + "id": "row_104", + "kind": "positive", + "upper_cent": 9500000, + "indicator": "taxable_income_band_row_104_indicator", + "source_label": "$94,001 - $95,000", + "count_reference": "ird_taxable_income_people_by_band__row_104", + "amount_reference": "ird_taxable_income_total_by_band__row_104" + }, + { + "id": "row_105", + "kind": "positive", + "upper_cent": 9600000, + "indicator": "taxable_income_band_row_105_indicator", + "source_label": "$95,001 - $96,000", + "count_reference": "ird_taxable_income_people_by_band__row_105", + "amount_reference": "ird_taxable_income_total_by_band__row_105" + }, + { + "id": "row_106", + "kind": "positive", + "upper_cent": 9700000, + "indicator": "taxable_income_band_row_106_indicator", + "source_label": "$96,001 - $97,000", + "count_reference": "ird_taxable_income_people_by_band__row_106", + "amount_reference": "ird_taxable_income_total_by_band__row_106" + }, + { + "id": "row_107", + "kind": "positive", + "upper_cent": 9800000, + "indicator": "taxable_income_band_row_107_indicator", + "source_label": "$97,001 - $98,000", + "count_reference": "ird_taxable_income_people_by_band__row_107", + "amount_reference": "ird_taxable_income_total_by_band__row_107" + }, + { + "id": "row_108", + "kind": "positive", + "upper_cent": 9900000, + "indicator": "taxable_income_band_row_108_indicator", + "source_label": "$98,001 - $99,000", + "count_reference": "ird_taxable_income_people_by_band__row_108", + "amount_reference": "ird_taxable_income_total_by_band__row_108" + }, + { + "id": "row_109", + "kind": "positive", + "upper_cent": 10000000, + "indicator": "taxable_income_band_row_109_indicator", + "source_label": "$99,001 - $100,000", + "count_reference": "ird_taxable_income_people_by_band__row_109", + "amount_reference": "ird_taxable_income_total_by_band__row_109" + }, + { + "id": "row_110", + "kind": "positive", + "upper_cent": 10100000, + "indicator": "taxable_income_band_row_110_indicator", + "source_label": "$100,001 - $101,000", + "count_reference": "ird_taxable_income_people_by_band__row_110", + "amount_reference": "ird_taxable_income_total_by_band__row_110" + }, + { + "id": "row_111", + "kind": "positive", + "upper_cent": 10200000, + "indicator": "taxable_income_band_row_111_indicator", + "source_label": "$101,001 - $102,000", + "count_reference": "ird_taxable_income_people_by_band__row_111", + "amount_reference": "ird_taxable_income_total_by_band__row_111" + }, + { + "id": "row_112", + "kind": "positive", + "upper_cent": 10300000, + "indicator": "taxable_income_band_row_112_indicator", + "source_label": "$102,001 - $103,000", + "count_reference": "ird_taxable_income_people_by_band__row_112", + "amount_reference": "ird_taxable_income_total_by_band__row_112" + }, + { + "id": "row_113", + "kind": "positive", + "upper_cent": 10400000, + "indicator": "taxable_income_band_row_113_indicator", + "source_label": "$103,001 - $104,000", + "count_reference": "ird_taxable_income_people_by_band__row_113", + "amount_reference": "ird_taxable_income_total_by_band__row_113" + }, + { + "id": "row_114", + "kind": "positive", + "upper_cent": 10500000, + "indicator": "taxable_income_band_row_114_indicator", + "source_label": "$104,001 - $105,000", + "count_reference": "ird_taxable_income_people_by_band__row_114", + "amount_reference": "ird_taxable_income_total_by_band__row_114" + }, + { + "id": "row_115", + "kind": "positive", + "upper_cent": 10600000, + "indicator": "taxable_income_band_row_115_indicator", + "source_label": "$105,001 - $106,000", + "count_reference": "ird_taxable_income_people_by_band__row_115", + "amount_reference": "ird_taxable_income_total_by_band__row_115" + }, + { + "id": "row_116", + "kind": "positive", + "upper_cent": 10700000, + "indicator": "taxable_income_band_row_116_indicator", + "source_label": "$106,001 - $107,000", + "count_reference": "ird_taxable_income_people_by_band__row_116", + "amount_reference": "ird_taxable_income_total_by_band__row_116" + }, + { + "id": "row_117", + "kind": "positive", + "upper_cent": 10800000, + "indicator": "taxable_income_band_row_117_indicator", + "source_label": "$107,001 - $108,000", + "count_reference": "ird_taxable_income_people_by_band__row_117", + "amount_reference": "ird_taxable_income_total_by_band__row_117" + }, + { + "id": "row_118", + "kind": "positive", + "upper_cent": 10900000, + "indicator": "taxable_income_band_row_118_indicator", + "source_label": "$108,001 - $109,000", + "count_reference": "ird_taxable_income_people_by_band__row_118", + "amount_reference": "ird_taxable_income_total_by_band__row_118" + }, + { + "id": "row_119", + "kind": "positive", + "upper_cent": 11000000, + "indicator": "taxable_income_band_row_119_indicator", + "source_label": "$109,001 - $110,000", + "count_reference": "ird_taxable_income_people_by_band__row_119", + "amount_reference": "ird_taxable_income_total_by_band__row_119" + }, + { + "id": "row_120", + "kind": "positive", + "upper_cent": 11100000, + "indicator": "taxable_income_band_row_120_indicator", + "source_label": "$110,001 - $111,000", + "count_reference": "ird_taxable_income_people_by_band__row_120", + "amount_reference": "ird_taxable_income_total_by_band__row_120" + }, + { + "id": "row_121", + "kind": "positive", + "upper_cent": 11200000, + "indicator": "taxable_income_band_row_121_indicator", + "source_label": "$111,001 - $112,000", + "count_reference": "ird_taxable_income_people_by_band__row_121", + "amount_reference": "ird_taxable_income_total_by_band__row_121" + }, + { + "id": "row_122", + "kind": "positive", + "upper_cent": 11300000, + "indicator": "taxable_income_band_row_122_indicator", + "source_label": "$112,001 - $113,000", + "count_reference": "ird_taxable_income_people_by_band__row_122", + "amount_reference": "ird_taxable_income_total_by_band__row_122" + }, + { + "id": "row_123", + "kind": "positive", + "upper_cent": 11400000, + "indicator": "taxable_income_band_row_123_indicator", + "source_label": "$113,001 - $114,000", + "count_reference": "ird_taxable_income_people_by_band__row_123", + "amount_reference": "ird_taxable_income_total_by_band__row_123" + }, + { + "id": "row_124", + "kind": "positive", + "upper_cent": 11500000, + "indicator": "taxable_income_band_row_124_indicator", + "source_label": "$114,001 - $115,000", + "count_reference": "ird_taxable_income_people_by_band__row_124", + "amount_reference": "ird_taxable_income_total_by_band__row_124" + }, + { + "id": "row_125", + "kind": "positive", + "upper_cent": 11600000, + "indicator": "taxable_income_band_row_125_indicator", + "source_label": "$115,001 - $116,000", + "count_reference": "ird_taxable_income_people_by_band__row_125", + "amount_reference": "ird_taxable_income_total_by_band__row_125" + }, + { + "id": "row_126", + "kind": "positive", + "upper_cent": 11700000, + "indicator": "taxable_income_band_row_126_indicator", + "source_label": "$116,001 - $117,000", + "count_reference": "ird_taxable_income_people_by_band__row_126", + "amount_reference": "ird_taxable_income_total_by_band__row_126" + }, + { + "id": "row_127", + "kind": "positive", + "upper_cent": 11800000, + "indicator": "taxable_income_band_row_127_indicator", + "source_label": "$117,001 - $118,000", + "count_reference": "ird_taxable_income_people_by_band__row_127", + "amount_reference": "ird_taxable_income_total_by_band__row_127" + }, + { + "id": "row_128", + "kind": "positive", + "upper_cent": 11900000, + "indicator": "taxable_income_band_row_128_indicator", + "source_label": "$118,001 - $119,000", + "count_reference": "ird_taxable_income_people_by_band__row_128", + "amount_reference": "ird_taxable_income_total_by_band__row_128" + }, + { + "id": "row_129", + "kind": "positive", + "upper_cent": 12000000, + "indicator": "taxable_income_band_row_129_indicator", + "source_label": "$119,001 - $120,000", + "count_reference": "ird_taxable_income_people_by_band__row_129", + "amount_reference": "ird_taxable_income_total_by_band__row_129" + }, + { + "id": "row_130", + "kind": "positive", + "upper_cent": 12100000, + "indicator": "taxable_income_band_row_130_indicator", + "source_label": "$120,001 - $121,000", + "count_reference": "ird_taxable_income_people_by_band__row_130", + "amount_reference": "ird_taxable_income_total_by_band__row_130" + }, + { + "id": "row_131", + "kind": "positive", + "upper_cent": 12200000, + "indicator": "taxable_income_band_row_131_indicator", + "source_label": "$121,001 - $122,000", + "count_reference": "ird_taxable_income_people_by_band__row_131", + "amount_reference": "ird_taxable_income_total_by_band__row_131" + }, + { + "id": "row_132", + "kind": "positive", + "upper_cent": 12300000, + "indicator": "taxable_income_band_row_132_indicator", + "source_label": "$122,001 - $123,000", + "count_reference": "ird_taxable_income_people_by_band__row_132", + "amount_reference": "ird_taxable_income_total_by_band__row_132" + }, + { + "id": "row_133", + "kind": "positive", + "upper_cent": 12400000, + "indicator": "taxable_income_band_row_133_indicator", + "source_label": "$123,001 - $124,000", + "count_reference": "ird_taxable_income_people_by_band__row_133", + "amount_reference": "ird_taxable_income_total_by_band__row_133" + }, + { + "id": "row_134", + "kind": "positive", + "upper_cent": 12500000, + "indicator": "taxable_income_band_row_134_indicator", + "source_label": "$124,001 - $125,000", + "count_reference": "ird_taxable_income_people_by_band__row_134", + "amount_reference": "ird_taxable_income_total_by_band__row_134" + }, + { + "id": "row_135", + "kind": "positive", + "upper_cent": 12600000, + "indicator": "taxable_income_band_row_135_indicator", + "source_label": "$125,001 - $126,000", + "count_reference": "ird_taxable_income_people_by_band__row_135", + "amount_reference": "ird_taxable_income_total_by_band__row_135" + }, + { + "id": "row_136", + "kind": "positive", + "upper_cent": 12700000, + "indicator": "taxable_income_band_row_136_indicator", + "source_label": "$126,001 - $127,000", + "count_reference": "ird_taxable_income_people_by_band__row_136", + "amount_reference": "ird_taxable_income_total_by_band__row_136" + }, + { + "id": "row_137", + "kind": "positive", + "upper_cent": 12800000, + "indicator": "taxable_income_band_row_137_indicator", + "source_label": "$127,001 - $128,000", + "count_reference": "ird_taxable_income_people_by_band__row_137", + "amount_reference": "ird_taxable_income_total_by_band__row_137" + }, + { + "id": "row_138", + "kind": "positive", + "upper_cent": 12900000, + "indicator": "taxable_income_band_row_138_indicator", + "source_label": "$128,001 - $129,000", + "count_reference": "ird_taxable_income_people_by_band__row_138", + "amount_reference": "ird_taxable_income_total_by_band__row_138" + }, + { + "id": "row_139", + "kind": "positive", + "upper_cent": 13000000, + "indicator": "taxable_income_band_row_139_indicator", + "source_label": "$129,001 - $130,000", + "count_reference": "ird_taxable_income_people_by_band__row_139", + "amount_reference": "ird_taxable_income_total_by_band__row_139" + }, + { + "id": "row_140", + "kind": "positive", + "upper_cent": 13100000, + "indicator": "taxable_income_band_row_140_indicator", + "source_label": "$130,001 - $131,000", + "count_reference": "ird_taxable_income_people_by_band__row_140", + "amount_reference": "ird_taxable_income_total_by_band__row_140" + }, + { + "id": "row_141", + "kind": "positive", + "upper_cent": 13200000, + "indicator": "taxable_income_band_row_141_indicator", + "source_label": "$131,001 - $132,000", + "count_reference": "ird_taxable_income_people_by_band__row_141", + "amount_reference": "ird_taxable_income_total_by_band__row_141" + }, + { + "id": "row_142", + "kind": "positive", + "upper_cent": 13300000, + "indicator": "taxable_income_band_row_142_indicator", + "source_label": "$132,001 - $133,000", + "count_reference": "ird_taxable_income_people_by_band__row_142", + "amount_reference": "ird_taxable_income_total_by_band__row_142" + }, + { + "id": "row_143", + "kind": "positive", + "upper_cent": 13400000, + "indicator": "taxable_income_band_row_143_indicator", + "source_label": "$133,001 - $134,000", + "count_reference": "ird_taxable_income_people_by_band__row_143", + "amount_reference": "ird_taxable_income_total_by_band__row_143" + }, + { + "id": "row_144", + "kind": "positive", + "upper_cent": 13500000, + "indicator": "taxable_income_band_row_144_indicator", + "source_label": "$134,001 - $135,000", + "count_reference": "ird_taxable_income_people_by_band__row_144", + "amount_reference": "ird_taxable_income_total_by_band__row_144" + }, + { + "id": "row_145", + "kind": "positive", + "upper_cent": 13600000, + "indicator": "taxable_income_band_row_145_indicator", + "source_label": "$135,001 - $136,000", + "count_reference": "ird_taxable_income_people_by_band__row_145", + "amount_reference": "ird_taxable_income_total_by_band__row_145" + }, + { + "id": "row_146", + "kind": "positive", + "upper_cent": 13700000, + "indicator": "taxable_income_band_row_146_indicator", + "source_label": "$136,001 - $137,000", + "count_reference": "ird_taxable_income_people_by_band__row_146", + "amount_reference": "ird_taxable_income_total_by_band__row_146" + }, + { + "id": "row_147", + "kind": "positive", + "upper_cent": 13800000, + "indicator": "taxable_income_band_row_147_indicator", + "source_label": "$137,001 - $138,000", + "count_reference": "ird_taxable_income_people_by_band__row_147", + "amount_reference": "ird_taxable_income_total_by_band__row_147" + }, + { + "id": "row_148", + "kind": "positive", + "upper_cent": 13900000, + "indicator": "taxable_income_band_row_148_indicator", + "source_label": "$138,001 - $139,000", + "count_reference": "ird_taxable_income_people_by_band__row_148", + "amount_reference": "ird_taxable_income_total_by_band__row_148" + }, + { + "id": "row_149", + "kind": "positive", + "upper_cent": 14000000, + "indicator": "taxable_income_band_row_149_indicator", + "source_label": "$139,001 - $140,000", + "count_reference": "ird_taxable_income_people_by_band__row_149", + "amount_reference": "ird_taxable_income_total_by_band__row_149" + }, + { + "id": "row_150", + "kind": "positive", + "upper_cent": 14100000, + "indicator": "taxable_income_band_row_150_indicator", + "source_label": "$140,001 - $141,000", + "count_reference": "ird_taxable_income_people_by_band__row_150", + "amount_reference": "ird_taxable_income_total_by_band__row_150" + }, + { + "id": "row_151", + "kind": "positive", + "upper_cent": 14200000, + "indicator": "taxable_income_band_row_151_indicator", + "source_label": "$141,001 - $142,000", + "count_reference": "ird_taxable_income_people_by_band__row_151", + "amount_reference": "ird_taxable_income_total_by_band__row_151" + }, + { + "id": "row_152", + "kind": "positive", + "upper_cent": 14300000, + "indicator": "taxable_income_band_row_152_indicator", + "source_label": "$142,001 - $143,000", + "count_reference": "ird_taxable_income_people_by_band__row_152", + "amount_reference": "ird_taxable_income_total_by_band__row_152" + }, + { + "id": "row_153", + "kind": "positive", + "upper_cent": 14400000, + "indicator": "taxable_income_band_row_153_indicator", + "source_label": "$143,001 - $144,000", + "count_reference": "ird_taxable_income_people_by_band__row_153", + "amount_reference": "ird_taxable_income_total_by_band__row_153" + }, + { + "id": "row_154", + "kind": "positive", + "upper_cent": 14500000, + "indicator": "taxable_income_band_row_154_indicator", + "source_label": "$144,001 - $145,000", + "count_reference": "ird_taxable_income_people_by_band__row_154", + "amount_reference": "ird_taxable_income_total_by_band__row_154" + }, + { + "id": "row_155", + "kind": "positive", + "upper_cent": 14600000, + "indicator": "taxable_income_band_row_155_indicator", + "source_label": "$145,001 - $146,000", + "count_reference": "ird_taxable_income_people_by_band__row_155", + "amount_reference": "ird_taxable_income_total_by_band__row_155" + }, + { + "id": "row_156", + "kind": "positive", + "upper_cent": 14700000, + "indicator": "taxable_income_band_row_156_indicator", + "source_label": "$146,001 - $147,000", + "count_reference": "ird_taxable_income_people_by_band__row_156", + "amount_reference": "ird_taxable_income_total_by_band__row_156" + }, + { + "id": "row_157", + "kind": "positive", + "upper_cent": 14800000, + "indicator": "taxable_income_band_row_157_indicator", + "source_label": "$147,001 - $148,000", + "count_reference": "ird_taxable_income_people_by_band__row_157", + "amount_reference": "ird_taxable_income_total_by_band__row_157" + }, + { + "id": "row_158", + "kind": "positive", + "upper_cent": 14900000, + "indicator": "taxable_income_band_row_158_indicator", + "source_label": "$148,001 - $149,000", + "count_reference": "ird_taxable_income_people_by_band__row_158", + "amount_reference": "ird_taxable_income_total_by_band__row_158" + }, + { + "id": "row_159", + "kind": "positive", + "upper_cent": 15000000, + "indicator": "taxable_income_band_row_159_indicator", + "source_label": "$149,001 - $150,000", + "count_reference": "ird_taxable_income_people_by_band__row_159", + "amount_reference": "ird_taxable_income_total_by_band__row_159" + }, + { + "id": "row_160", + "kind": "positive", + "upper_cent": 15100000, + "indicator": "taxable_income_band_row_160_indicator", + "source_label": "$150,001 - $151,000", + "count_reference": "ird_taxable_income_people_by_band__row_160", + "amount_reference": "ird_taxable_income_total_by_band__row_160" + }, + { + "id": "row_161", + "kind": "positive", + "upper_cent": 15200000, + "indicator": "taxable_income_band_row_161_indicator", + "source_label": "$151,001 - $152,000", + "count_reference": "ird_taxable_income_people_by_band__row_161", + "amount_reference": "ird_taxable_income_total_by_band__row_161" + }, + { + "id": "row_162", + "kind": "positive", + "upper_cent": 15300000, + "indicator": "taxable_income_band_row_162_indicator", + "source_label": "$152,001 - $153,000", + "count_reference": "ird_taxable_income_people_by_band__row_162", + "amount_reference": "ird_taxable_income_total_by_band__row_162" + }, + { + "id": "row_163", + "kind": "positive", + "upper_cent": 15400000, + "indicator": "taxable_income_band_row_163_indicator", + "source_label": "$153,001 - $154,000", + "count_reference": "ird_taxable_income_people_by_band__row_163", + "amount_reference": "ird_taxable_income_total_by_band__row_163" + }, + { + "id": "row_164", + "kind": "positive", + "upper_cent": 15500000, + "indicator": "taxable_income_band_row_164_indicator", + "source_label": "$154,001 - $155,000", + "count_reference": "ird_taxable_income_people_by_band__row_164", + "amount_reference": "ird_taxable_income_total_by_band__row_164" + }, + { + "id": "row_165", + "kind": "positive", + "upper_cent": 15600000, + "indicator": "taxable_income_band_row_165_indicator", + "source_label": "$155,001 - $156,000", + "count_reference": "ird_taxable_income_people_by_band__row_165", + "amount_reference": "ird_taxable_income_total_by_band__row_165" + }, + { + "id": "row_166", + "kind": "positive", + "upper_cent": 15700000, + "indicator": "taxable_income_band_row_166_indicator", + "source_label": "$156,001 - $157,000", + "count_reference": "ird_taxable_income_people_by_band__row_166", + "amount_reference": "ird_taxable_income_total_by_band__row_166" + }, + { + "id": "row_167", + "kind": "positive", + "upper_cent": 15800000, + "indicator": "taxable_income_band_row_167_indicator", + "source_label": "$157,001 - $158,000", + "count_reference": "ird_taxable_income_people_by_band__row_167", + "amount_reference": "ird_taxable_income_total_by_band__row_167" + }, + { + "id": "row_168", + "kind": "positive", + "upper_cent": 15900000, + "indicator": "taxable_income_band_row_168_indicator", + "source_label": "$158,001 - $159,000", + "count_reference": "ird_taxable_income_people_by_band__row_168", + "amount_reference": "ird_taxable_income_total_by_band__row_168" + }, + { + "id": "row_169", + "kind": "positive", + "upper_cent": 16000000, + "indicator": "taxable_income_band_row_169_indicator", + "source_label": "$159,001 - $160,000", + "count_reference": "ird_taxable_income_people_by_band__row_169", + "amount_reference": "ird_taxable_income_total_by_band__row_169" + }, + { + "id": "row_170", + "kind": "positive", + "upper_cent": 16100000, + "indicator": "taxable_income_band_row_170_indicator", + "source_label": "$160,001 - $161,000", + "count_reference": "ird_taxable_income_people_by_band__row_170", + "amount_reference": "ird_taxable_income_total_by_band__row_170" + }, + { + "id": "row_171", + "kind": "positive", + "upper_cent": 16200000, + "indicator": "taxable_income_band_row_171_indicator", + "source_label": "$161,001 - $162,000", + "count_reference": "ird_taxable_income_people_by_band__row_171", + "amount_reference": "ird_taxable_income_total_by_band__row_171" + }, + { + "id": "row_172", + "kind": "positive", + "upper_cent": 16300000, + "indicator": "taxable_income_band_row_172_indicator", + "source_label": "$162,001 - $163,000", + "count_reference": "ird_taxable_income_people_by_band__row_172", + "amount_reference": "ird_taxable_income_total_by_band__row_172" + }, + { + "id": "row_173", + "kind": "positive", + "upper_cent": 16400000, + "indicator": "taxable_income_band_row_173_indicator", + "source_label": "$163,001 - $164,000", + "count_reference": "ird_taxable_income_people_by_band__row_173", + "amount_reference": "ird_taxable_income_total_by_band__row_173" + }, + { + "id": "row_174", + "kind": "positive", + "upper_cent": 16500000, + "indicator": "taxable_income_band_row_174_indicator", + "source_label": "$164,001 - $165,000", + "count_reference": "ird_taxable_income_people_by_band__row_174", + "amount_reference": "ird_taxable_income_total_by_band__row_174" + }, + { + "id": "row_175", + "kind": "positive", + "upper_cent": 16600000, + "indicator": "taxable_income_band_row_175_indicator", + "source_label": "$165,001 - $166,000", + "count_reference": "ird_taxable_income_people_by_band__row_175", + "amount_reference": "ird_taxable_income_total_by_band__row_175" + }, + { + "id": "row_176", + "kind": "positive", + "upper_cent": 16700000, + "indicator": "taxable_income_band_row_176_indicator", + "source_label": "$166,001 - $167,000", + "count_reference": "ird_taxable_income_people_by_band__row_176", + "amount_reference": "ird_taxable_income_total_by_band__row_176" + }, + { + "id": "row_177", + "kind": "positive", + "upper_cent": 16800000, + "indicator": "taxable_income_band_row_177_indicator", + "source_label": "$167,001 - $168,000", + "count_reference": "ird_taxable_income_people_by_band__row_177", + "amount_reference": "ird_taxable_income_total_by_band__row_177" + }, + { + "id": "row_178", + "kind": "positive", + "upper_cent": 16900000, + "indicator": "taxable_income_band_row_178_indicator", + "source_label": "$168,001 - $169,000", + "count_reference": "ird_taxable_income_people_by_band__row_178", + "amount_reference": "ird_taxable_income_total_by_band__row_178" + }, + { + "id": "row_179", + "kind": "positive", + "upper_cent": 17000000, + "indicator": "taxable_income_band_row_179_indicator", + "source_label": "$169,001 - $170,000", + "count_reference": "ird_taxable_income_people_by_band__row_179", + "amount_reference": "ird_taxable_income_total_by_band__row_179" + }, + { + "id": "row_180", + "kind": "positive", + "upper_cent": 17100000, + "indicator": "taxable_income_band_row_180_indicator", + "source_label": "$170,001 - $171,000", + "count_reference": "ird_taxable_income_people_by_band__row_180", + "amount_reference": "ird_taxable_income_total_by_band__row_180" + }, + { + "id": "row_181", + "kind": "positive", + "upper_cent": 17200000, + "indicator": "taxable_income_band_row_181_indicator", + "source_label": "$171,001 - $172,000", + "count_reference": "ird_taxable_income_people_by_band__row_181", + "amount_reference": "ird_taxable_income_total_by_band__row_181" + }, + { + "id": "row_182", + "kind": "positive", + "upper_cent": 17300000, + "indicator": "taxable_income_band_row_182_indicator", + "source_label": "$172,001 - $173,000", + "count_reference": "ird_taxable_income_people_by_band__row_182", + "amount_reference": "ird_taxable_income_total_by_band__row_182" + }, + { + "id": "row_183", + "kind": "positive", + "upper_cent": 17400000, + "indicator": "taxable_income_band_row_183_indicator", + "source_label": "$173,001 - $174,000", + "count_reference": "ird_taxable_income_people_by_band__row_183", + "amount_reference": "ird_taxable_income_total_by_band__row_183" + }, + { + "id": "row_184", + "kind": "positive", + "upper_cent": 17500000, + "indicator": "taxable_income_band_row_184_indicator", + "source_label": "$174,001 - $175,000", + "count_reference": "ird_taxable_income_people_by_band__row_184", + "amount_reference": "ird_taxable_income_total_by_band__row_184" + }, + { + "id": "row_185", + "kind": "positive", + "upper_cent": 17600000, + "indicator": "taxable_income_band_row_185_indicator", + "source_label": "$175,001 - $176,000", + "count_reference": "ird_taxable_income_people_by_band__row_185", + "amount_reference": "ird_taxable_income_total_by_band__row_185" + }, + { + "id": "row_186", + "kind": "positive", + "upper_cent": 17700000, + "indicator": "taxable_income_band_row_186_indicator", + "source_label": "$176,001 - $177,000", + "count_reference": "ird_taxable_income_people_by_band__row_186", + "amount_reference": "ird_taxable_income_total_by_band__row_186" + }, + { + "id": "row_187", + "kind": "positive", + "upper_cent": 17800000, + "indicator": "taxable_income_band_row_187_indicator", + "source_label": "$177,001 - $178,000", + "count_reference": "ird_taxable_income_people_by_band__row_187", + "amount_reference": "ird_taxable_income_total_by_band__row_187" + }, + { + "id": "row_188", + "kind": "positive", + "upper_cent": 17900000, + "indicator": "taxable_income_band_row_188_indicator", + "source_label": "$178,001 - $179,000", + "count_reference": "ird_taxable_income_people_by_band__row_188", + "amount_reference": "ird_taxable_income_total_by_band__row_188" + }, + { + "id": "row_189", + "kind": "positive", + "upper_cent": 18000000, + "indicator": "taxable_income_band_row_189_indicator", + "source_label": "$179,001 - $180,000", + "count_reference": "ird_taxable_income_people_by_band__row_189", + "amount_reference": "ird_taxable_income_total_by_band__row_189" + }, + { + "id": "row_190", + "kind": "positive", + "upper_cent": 18100000, + "indicator": "taxable_income_band_row_190_indicator", + "source_label": "$180,001 - $181,000", + "count_reference": "ird_taxable_income_people_by_band__row_190", + "amount_reference": "ird_taxable_income_total_by_band__row_190" + }, + { + "id": "row_191", + "kind": "positive", + "upper_cent": 18200000, + "indicator": "taxable_income_band_row_191_indicator", + "source_label": "$181,001 - $182,000", + "count_reference": "ird_taxable_income_people_by_band__row_191", + "amount_reference": "ird_taxable_income_total_by_band__row_191" + }, + { + "id": "row_192", + "kind": "positive", + "upper_cent": 18300000, + "indicator": "taxable_income_band_row_192_indicator", + "source_label": "$182,001 - $183,000", + "count_reference": "ird_taxable_income_people_by_band__row_192", + "amount_reference": "ird_taxable_income_total_by_band__row_192" + }, + { + "id": "row_193", + "kind": "positive", + "upper_cent": 18400000, + "indicator": "taxable_income_band_row_193_indicator", + "source_label": "$183,001 - $184,000", + "count_reference": "ird_taxable_income_people_by_band__row_193", + "amount_reference": "ird_taxable_income_total_by_band__row_193" + }, + { + "id": "row_194", + "kind": "positive", + "upper_cent": 18500000, + "indicator": "taxable_income_band_row_194_indicator", + "source_label": "$184,001 - $185,000", + "count_reference": "ird_taxable_income_people_by_band__row_194", + "amount_reference": "ird_taxable_income_total_by_band__row_194" + }, + { + "id": "row_195", + "kind": "positive", + "upper_cent": 18600000, + "indicator": "taxable_income_band_row_195_indicator", + "source_label": "$185,001 - $186,000", + "count_reference": "ird_taxable_income_people_by_band__row_195", + "amount_reference": "ird_taxable_income_total_by_band__row_195" + }, + { + "id": "row_196", + "kind": "positive", + "upper_cent": 18700000, + "indicator": "taxable_income_band_row_196_indicator", + "source_label": "$186,001 - $187,000", + "count_reference": "ird_taxable_income_people_by_band__row_196", + "amount_reference": "ird_taxable_income_total_by_band__row_196" + }, + { + "id": "row_197", + "kind": "positive", + "upper_cent": 18800000, + "indicator": "taxable_income_band_row_197_indicator", + "source_label": "$187,001 - $188,000", + "count_reference": "ird_taxable_income_people_by_band__row_197", + "amount_reference": "ird_taxable_income_total_by_band__row_197" + }, + { + "id": "row_198", + "kind": "positive", + "upper_cent": 18900000, + "indicator": "taxable_income_band_row_198_indicator", + "source_label": "$188,001 - $189,000", + "count_reference": "ird_taxable_income_people_by_band__row_198", + "amount_reference": "ird_taxable_income_total_by_band__row_198" + }, + { + "id": "row_199", + "kind": "positive", + "upper_cent": 19000000, + "indicator": "taxable_income_band_row_199_indicator", + "source_label": "$189,001 - $190,000", + "count_reference": "ird_taxable_income_people_by_band__row_199", + "amount_reference": "ird_taxable_income_total_by_band__row_199" + }, + { + "id": "row_200", + "kind": "positive", + "upper_cent": 19100000, + "indicator": "taxable_income_band_row_200_indicator", + "source_label": "$190,001 - $191,000", + "count_reference": "ird_taxable_income_people_by_band__row_200", + "amount_reference": "ird_taxable_income_total_by_band__row_200" + }, + { + "id": "row_201", + "kind": "positive", + "upper_cent": 19200000, + "indicator": "taxable_income_band_row_201_indicator", + "source_label": "$191,001 - $192,000", + "count_reference": "ird_taxable_income_people_by_band__row_201", + "amount_reference": "ird_taxable_income_total_by_band__row_201" + }, + { + "id": "row_202", + "kind": "positive", + "upper_cent": 19300000, + "indicator": "taxable_income_band_row_202_indicator", + "source_label": "$192,001 - $193,000", + "count_reference": "ird_taxable_income_people_by_band__row_202", + "amount_reference": "ird_taxable_income_total_by_band__row_202" + }, + { + "id": "row_203", + "kind": "positive", + "upper_cent": 19400000, + "indicator": "taxable_income_band_row_203_indicator", + "source_label": "$193,001 - $194,000", + "count_reference": "ird_taxable_income_people_by_band__row_203", + "amount_reference": "ird_taxable_income_total_by_band__row_203" + }, + { + "id": "row_204", + "kind": "positive", + "upper_cent": 19500000, + "indicator": "taxable_income_band_row_204_indicator", + "source_label": "$194,001 - $195,000", + "count_reference": "ird_taxable_income_people_by_band__row_204", + "amount_reference": "ird_taxable_income_total_by_band__row_204" + }, + { + "id": "row_205", + "kind": "positive", + "upper_cent": 19600000, + "indicator": "taxable_income_band_row_205_indicator", + "source_label": "$195,001 - $196,000", + "count_reference": "ird_taxable_income_people_by_band__row_205", + "amount_reference": "ird_taxable_income_total_by_band__row_205" + }, + { + "id": "row_206", + "kind": "positive", + "upper_cent": 19700000, + "indicator": "taxable_income_band_row_206_indicator", + "source_label": "$196,001 - $197,000", + "count_reference": "ird_taxable_income_people_by_band__row_206", + "amount_reference": "ird_taxable_income_total_by_band__row_206" + }, + { + "id": "row_207", + "kind": "positive", + "upper_cent": 19800000, + "indicator": "taxable_income_band_row_207_indicator", + "source_label": "$197,001 - $198,000", + "count_reference": "ird_taxable_income_people_by_band__row_207", + "amount_reference": "ird_taxable_income_total_by_band__row_207" + }, + { + "id": "row_208", + "kind": "positive", + "upper_cent": 19900000, + "indicator": "taxable_income_band_row_208_indicator", + "source_label": "$198,001 - $199,000", + "count_reference": "ird_taxable_income_people_by_band__row_208", + "amount_reference": "ird_taxable_income_total_by_band__row_208" + }, + { + "id": "row_209", + "kind": "positive", + "upper_cent": 20000000, + "indicator": "taxable_income_band_row_209_indicator", + "source_label": "$199,001 - $200,000", + "count_reference": "ird_taxable_income_people_by_band__row_209", + "amount_reference": "ird_taxable_income_total_by_band__row_209" + }, + { + "id": "row_210", + "kind": "positive", + "upper_cent": 20500000, + "indicator": "taxable_income_band_row_210_indicator", + "source_label": "$200,001 - $205,000", + "count_reference": "ird_taxable_income_people_by_band__row_210", + "amount_reference": "ird_taxable_income_total_by_band__row_210" + }, + { + "id": "row_211", + "kind": "positive", + "upper_cent": 21000000, + "indicator": "taxable_income_band_row_211_indicator", + "source_label": "$205,001 - $210,000", + "count_reference": "ird_taxable_income_people_by_band__row_211", + "amount_reference": "ird_taxable_income_total_by_band__row_211" + }, + { + "id": "row_212", + "kind": "positive", + "upper_cent": 21500000, + "indicator": "taxable_income_band_row_212_indicator", + "source_label": "$210,001 - $215,000", + "count_reference": "ird_taxable_income_people_by_band__row_212", + "amount_reference": "ird_taxable_income_total_by_band__row_212" + }, + { + "id": "row_213", + "kind": "positive", + "upper_cent": 22000000, + "indicator": "taxable_income_band_row_213_indicator", + "source_label": "$215,001 - $220,000", + "count_reference": "ird_taxable_income_people_by_band__row_213", + "amount_reference": "ird_taxable_income_total_by_band__row_213" + }, + { + "id": "row_214", + "kind": "positive", + "upper_cent": 22500000, + "indicator": "taxable_income_band_row_214_indicator", + "source_label": "$220,001 - $225,000", + "count_reference": "ird_taxable_income_people_by_band__row_214", + "amount_reference": "ird_taxable_income_total_by_band__row_214" + }, + { + "id": "row_215", + "kind": "positive", + "upper_cent": 23000000, + "indicator": "taxable_income_band_row_215_indicator", + "source_label": "$225,001 - $230,000", + "count_reference": "ird_taxable_income_people_by_band__row_215", + "amount_reference": "ird_taxable_income_total_by_band__row_215" + }, + { + "id": "row_216", + "kind": "positive", + "upper_cent": 23500000, + "indicator": "taxable_income_band_row_216_indicator", + "source_label": "$230,001 - $235,000", + "count_reference": "ird_taxable_income_people_by_band__row_216", + "amount_reference": "ird_taxable_income_total_by_band__row_216" + }, + { + "id": "row_217", + "kind": "positive", + "upper_cent": 24000000, + "indicator": "taxable_income_band_row_217_indicator", + "source_label": "$235,001 - $240,000", + "count_reference": "ird_taxable_income_people_by_band__row_217", + "amount_reference": "ird_taxable_income_total_by_band__row_217" + }, + { + "id": "row_218", + "kind": "positive", + "upper_cent": 24500000, + "indicator": "taxable_income_band_row_218_indicator", + "source_label": "$240,001 - $245,000", + "count_reference": "ird_taxable_income_people_by_band__row_218", + "amount_reference": "ird_taxable_income_total_by_band__row_218" + }, + { + "id": "row_219", + "kind": "positive", + "upper_cent": 25000000, + "indicator": "taxable_income_band_row_219_indicator", + "source_label": "$245,001 - $250,000", + "count_reference": "ird_taxable_income_people_by_band__row_219", + "amount_reference": "ird_taxable_income_total_by_band__row_219" + }, + { + "id": "row_220", + "kind": "positive", + "upper_cent": 25500000, + "indicator": "taxable_income_band_row_220_indicator", + "source_label": "$250,001 - $255,000", + "count_reference": "ird_taxable_income_people_by_band__row_220", + "amount_reference": "ird_taxable_income_total_by_band__row_220" + }, + { + "id": "row_221", + "kind": "positive", + "upper_cent": 26000000, + "indicator": "taxable_income_band_row_221_indicator", + "source_label": "$255,001 - $260,000", + "count_reference": "ird_taxable_income_people_by_band__row_221", + "amount_reference": "ird_taxable_income_total_by_band__row_221" + }, + { + "id": "row_222", + "kind": "positive", + "upper_cent": 26500000, + "indicator": "taxable_income_band_row_222_indicator", + "source_label": "$260,001 - $265,000", + "count_reference": "ird_taxable_income_people_by_band__row_222", + "amount_reference": "ird_taxable_income_total_by_band__row_222" + }, + { + "id": "row_223", + "kind": "positive", + "upper_cent": 27000000, + "indicator": "taxable_income_band_row_223_indicator", + "source_label": "$265,001 - $270,000", + "count_reference": "ird_taxable_income_people_by_band__row_223", + "amount_reference": "ird_taxable_income_total_by_band__row_223" + }, + { + "id": "row_224", + "kind": "positive", + "upper_cent": 27500000, + "indicator": "taxable_income_band_row_224_indicator", + "source_label": "$270,001 - $275,000", + "count_reference": "ird_taxable_income_people_by_band__row_224", + "amount_reference": "ird_taxable_income_total_by_band__row_224" + }, + { + "id": "row_225", + "kind": "positive", + "upper_cent": 28000000, + "indicator": "taxable_income_band_row_225_indicator", + "source_label": "$275,001 - $280,000", + "count_reference": "ird_taxable_income_people_by_band__row_225", + "amount_reference": "ird_taxable_income_total_by_band__row_225" + }, + { + "id": "row_226", + "kind": "positive", + "upper_cent": 28500000, + "indicator": "taxable_income_band_row_226_indicator", + "source_label": "$280,001 - $285,000", + "count_reference": "ird_taxable_income_people_by_band__row_226", + "amount_reference": "ird_taxable_income_total_by_band__row_226" + }, + { + "id": "row_227", + "kind": "positive", + "upper_cent": 29000000, + "indicator": "taxable_income_band_row_227_indicator", + "source_label": "$285,001 - $290,000", + "count_reference": "ird_taxable_income_people_by_band__row_227", + "amount_reference": "ird_taxable_income_total_by_band__row_227" + }, + { + "id": "row_228", + "kind": "positive", + "upper_cent": 29500000, + "indicator": "taxable_income_band_row_228_indicator", + "source_label": "$290,001 - $295,000", + "count_reference": "ird_taxable_income_people_by_band__row_228", + "amount_reference": "ird_taxable_income_total_by_band__row_228" + }, + { + "id": "row_229", + "kind": "positive", + "upper_cent": 30000000, + "indicator": "taxable_income_band_row_229_indicator", + "source_label": "$295,001 - $300,000", + "count_reference": "ird_taxable_income_people_by_band__row_229", + "amount_reference": "ird_taxable_income_total_by_band__row_229" + }, + { + "id": "row_230", + "kind": "positive", + "upper_cent": 35000000, + "indicator": "taxable_income_band_row_230_indicator", + "source_label": "$300,000 - $350,000", + "count_reference": "ird_taxable_income_people_by_band__row_230", + "amount_reference": "ird_taxable_income_total_by_band__row_230" + }, + { + "id": "row_231", + "kind": "positive", + "upper_cent": 40000000, + "indicator": "taxable_income_band_row_231_indicator", + "source_label": "$350,001 - $400,000", + "count_reference": "ird_taxable_income_people_by_band__row_231", + "amount_reference": "ird_taxable_income_total_by_band__row_231" + }, + { + "id": "row_232", + "kind": "positive", + "upper_cent": 45000000, + "indicator": "taxable_income_band_row_232_indicator", + "source_label": "$400,001 - $450,000", + "count_reference": "ird_taxable_income_people_by_band__row_232", + "amount_reference": "ird_taxable_income_total_by_band__row_232" + }, + { + "id": "row_233", + "kind": "positive", + "upper_cent": 50000000, + "indicator": "taxable_income_band_row_233_indicator", + "source_label": "$450,001 - $500,000", + "count_reference": "ird_taxable_income_people_by_band__row_233", + "amount_reference": "ird_taxable_income_total_by_band__row_233" + }, + { + "id": "row_234", + "kind": "positive", + "upper_cent": 55000000, + "indicator": "taxable_income_band_row_234_indicator", + "source_label": "$500,001 - $550,000", + "count_reference": "ird_taxable_income_people_by_band__row_234", + "amount_reference": "ird_taxable_income_total_by_band__row_234" + }, + { + "id": "row_235", + "kind": "positive", + "upper_cent": 60000000, + "indicator": "taxable_income_band_row_235_indicator", + "source_label": "$550,001 - $600,000", + "count_reference": "ird_taxable_income_people_by_band__row_235", + "amount_reference": "ird_taxable_income_total_by_band__row_235" + }, + { + "id": "row_236", + "kind": "positive", + "upper_cent": 65000000, + "indicator": "taxable_income_band_row_236_indicator", + "source_label": "$600,001 - $650,000", + "count_reference": "ird_taxable_income_people_by_band__row_236", + "amount_reference": "ird_taxable_income_total_by_band__row_236" + }, + { + "id": "row_237", + "kind": "positive", + "upper_cent": 70000000, + "indicator": "taxable_income_band_row_237_indicator", + "source_label": "$650,001 - $700,000", + "count_reference": "ird_taxable_income_people_by_band__row_237", + "amount_reference": "ird_taxable_income_total_by_band__row_237" + }, + { + "id": "row_238", + "kind": "positive", + "upper_cent": 75000000, + "indicator": "taxable_income_band_row_238_indicator", + "source_label": "$700,001 - $750,000", + "count_reference": "ird_taxable_income_people_by_band__row_238", + "amount_reference": "ird_taxable_income_total_by_band__row_238" + }, + { + "id": "row_239", + "kind": "positive", + "upper_cent": 80000000, + "indicator": "taxable_income_band_row_239_indicator", + "source_label": "$750,001 - $800,000", + "count_reference": "ird_taxable_income_people_by_band__row_239", + "amount_reference": "ird_taxable_income_total_by_band__row_239" + }, + { + "id": "row_240", + "kind": "positive", + "upper_cent": 85000000, + "indicator": "taxable_income_band_row_240_indicator", + "source_label": "$800,001 - $850,000", + "count_reference": "ird_taxable_income_people_by_band__row_240", + "amount_reference": "ird_taxable_income_total_by_band__row_240" + }, + { + "id": "row_241", + "kind": "positive", + "upper_cent": 90000000, + "indicator": "taxable_income_band_row_241_indicator", + "source_label": "$850,001 - $900,000", + "count_reference": "ird_taxable_income_people_by_band__row_241", + "amount_reference": "ird_taxable_income_total_by_band__row_241" + }, + { + "id": "row_242", + "kind": "positive", + "upper_cent": 95000000, + "indicator": "taxable_income_band_row_242_indicator", + "source_label": "$900,001 - $950,000", + "count_reference": "ird_taxable_income_people_by_band__row_242", + "amount_reference": "ird_taxable_income_total_by_band__row_242" + }, + { + "id": "row_243", + "kind": "positive", + "upper_cent": 100000000, + "indicator": "taxable_income_band_row_243_indicator", + "source_label": "$950,001 - $1,000,000", + "count_reference": "ird_taxable_income_people_by_band__row_243", + "amount_reference": "ird_taxable_income_total_by_band__row_243" + }, + { + "id": "row_244", + "kind": "positive", + "upper_cent": null, + "indicator": "taxable_income_band_row_244_indicator", + "source_label": "Over $1,000,000", + "count_reference": "ird_taxable_income_people_by_band__row_244", + "amount_reference": "ird_taxable_income_total_by_band__row_244" + } + ], + "description": "National TY2024 taxable-income bands, rows 8-244: one nil band, 235 finite bands and the open tail over $1,000,000. Every band names its parked count and amount references." + }, + { + "id": "ird_taxable_income_age_ty2024", + "entity": "person", + "currency": "NZD", + "input_unit": "integer_cent", + "income_column": "ird_taxable_income_proxy_cents", + "universe_column": "ird_taxable_income_proxy_universe_status", + "source_period": 2024, + "target_period": 2027, + "positive_band_closure": "right", + "nil_band": "present", + "negative_income_policy": "floor_zero", + "first_lower_cut_cents": 0, + "source_locator": "A6:A188", + "diagnostics": { + "boundary_window_cents": 100, + "alternative_positive_band_closure": "left" + }, + "bands": [ + { + "id": "row_6", + "kind": "nil", + "indicator": "taxable_income_age_table_band_row_6_indicator", + "source_label": "nil" + }, + { + "id": "row_7", + "kind": "positive", + "upper_cent": 10000, + "indicator": "taxable_income_age_table_band_row_7_indicator", + "source_label": "$0.01 - $100" + }, + { + "id": "row_8", + "kind": "positive", + "upper_cent": 100000, + "indicator": "taxable_income_age_table_band_row_8_indicator", + "source_label": "$101 - $1,000" + }, + { + "id": "row_9", + "kind": "positive", + "upper_cent": 200000, + "indicator": "taxable_income_age_table_band_row_9_indicator", + "source_label": "$1,001 - $2,000" + }, + { + "id": "row_10", + "kind": "positive", + "upper_cent": 300000, + "indicator": "taxable_income_age_table_band_row_10_indicator", + "source_label": "$2,001 - $3,000" + }, + { + "id": "row_11", + "kind": "positive", + "upper_cent": 400000, + "indicator": "taxable_income_age_table_band_row_11_indicator", + "source_label": "$3,001 - $4,000" + }, + { + "id": "row_12", + "kind": "positive", + "upper_cent": 500000, + "indicator": "taxable_income_age_table_band_row_12_indicator", + "source_label": "$4,001 - $5,000" + }, + { + "id": "row_13", + "kind": "positive", + "upper_cent": 600000, + "indicator": "taxable_income_age_table_band_row_13_indicator", + "source_label": "$5,001 - $6,000" + }, + { + "id": "row_14", + "kind": "positive", + "upper_cent": 700000, + "indicator": "taxable_income_age_table_band_row_14_indicator", + "source_label": "$6,001 - $7,000" + }, + { + "id": "row_15", + "kind": "positive", + "upper_cent": 800000, + "indicator": "taxable_income_age_table_band_row_15_indicator", + "source_label": "$7,001 - $8,000" + }, + { + "id": "row_16", + "kind": "positive", + "upper_cent": 900000, + "indicator": "taxable_income_age_table_band_row_16_indicator", + "source_label": "$8,001 - $9,000" + }, + { + "id": "row_17", + "kind": "positive", + "upper_cent": 1000000, + "indicator": "taxable_income_age_table_band_row_17_indicator", + "source_label": "$9,001 - $10,000" + }, + { + "id": "row_18", + "kind": "positive", + "upper_cent": 1100000, + "indicator": "taxable_income_age_table_band_row_18_indicator", + "source_label": "$10,001 - $11,000" + }, + { + "id": "row_19", + "kind": "positive", + "upper_cent": 1200000, + "indicator": "taxable_income_age_table_band_row_19_indicator", + "source_label": "$11,001 - $12,000" + }, + { + "id": "row_20", + "kind": "positive", + "upper_cent": 1300000, + "indicator": "taxable_income_age_table_band_row_20_indicator", + "source_label": "$12,001 - $13,000" + }, + { + "id": "row_21", + "kind": "positive", + "upper_cent": 1400000, + "indicator": "taxable_income_age_table_band_row_21_indicator", + "source_label": "$13,001 - $14,000" + }, + { + "id": "row_22", + "kind": "positive", + "upper_cent": 1500000, + "indicator": "taxable_income_age_table_band_row_22_indicator", + "source_label": "$14,001 - $15,000" + }, + { + "id": "row_23", + "kind": "positive", + "upper_cent": 1600000, + "indicator": "taxable_income_age_table_band_row_23_indicator", + "source_label": "$15,001 - $16,000" + }, + { + "id": "row_24", + "kind": "positive", + "upper_cent": 1700000, + "indicator": "taxable_income_age_table_band_row_24_indicator", + "source_label": "$16,001 - $17,000" + }, + { + "id": "row_25", + "kind": "positive", + "upper_cent": 1800000, + "indicator": "taxable_income_age_table_band_row_25_indicator", + "source_label": "$17,001 - $18,000" + }, + { + "id": "row_26", + "kind": "positive", + "upper_cent": 1900000, + "indicator": "taxable_income_age_table_band_row_26_indicator", + "source_label": "$18,001 - $19,000" + }, + { + "id": "row_27", + "kind": "positive", + "upper_cent": 2000000, + "indicator": "taxable_income_age_table_band_row_27_indicator", + "source_label": "$19,001 - $20,000" + }, + { + "id": "row_28", + "kind": "positive", + "upper_cent": 2100000, + "indicator": "taxable_income_age_table_band_row_28_indicator", + "source_label": "$20,000 - $21,000" + }, + { + "id": "row_29", + "kind": "positive", + "upper_cent": 2200000, + "indicator": "taxable_income_age_table_band_row_29_indicator", + "source_label": "$21,001 - $22,000" + }, + { + "id": "row_30", + "kind": "positive", + "upper_cent": 2300000, + "indicator": "taxable_income_age_table_band_row_30_indicator", + "source_label": "$22,001 - $23,000" + }, + { + "id": "row_31", + "kind": "positive", + "upper_cent": 2400000, + "indicator": "taxable_income_age_table_band_row_31_indicator", + "source_label": "$23,001 - $24,000" + }, + { + "id": "row_32", + "kind": "positive", + "upper_cent": 2500000, + "indicator": "taxable_income_age_table_band_row_32_indicator", + "source_label": "$24,001 - $25,000" + }, + { + "id": "row_33", + "kind": "positive", + "upper_cent": 2600000, + "indicator": "taxable_income_age_table_band_row_33_indicator", + "source_label": "$25,001 - $26,000" + }, + { + "id": "row_34", + "kind": "positive", + "upper_cent": 2700000, + "indicator": "taxable_income_age_table_band_row_34_indicator", + "source_label": "$26,001 - $27,000" + }, + { + "id": "row_35", + "kind": "positive", + "upper_cent": 2800000, + "indicator": "taxable_income_age_table_band_row_35_indicator", + "source_label": "$27,001 - $28,000" + }, + { + "id": "row_36", + "kind": "positive", + "upper_cent": 2900000, + "indicator": "taxable_income_age_table_band_row_36_indicator", + "source_label": "$28,001 - $29,000" + }, + { + "id": "row_37", + "kind": "positive", + "upper_cent": 3000000, + "indicator": "taxable_income_age_table_band_row_37_indicator", + "source_label": "$29,001 - $30,000" + }, + { + "id": "row_38", + "kind": "positive", + "upper_cent": 3100000, + "indicator": "taxable_income_age_table_band_row_38_indicator", + "source_label": "$30,001 - $31,000" + }, + { + "id": "row_39", + "kind": "positive", + "upper_cent": 3200000, + "indicator": "taxable_income_age_table_band_row_39_indicator", + "source_label": "$31,001 - $32,000" + }, + { + "id": "row_40", + "kind": "positive", + "upper_cent": 3300000, + "indicator": "taxable_income_age_table_band_row_40_indicator", + "source_label": "$32,001 - $33,000" + }, + { + "id": "row_41", + "kind": "positive", + "upper_cent": 3400000, + "indicator": "taxable_income_age_table_band_row_41_indicator", + "source_label": "$33,001 - $34,000" + }, + { + "id": "row_42", + "kind": "positive", + "upper_cent": 3500000, + "indicator": "taxable_income_age_table_band_row_42_indicator", + "source_label": "$34,001 - $35,000" + }, + { + "id": "row_43", + "kind": "positive", + "upper_cent": 3600000, + "indicator": "taxable_income_age_table_band_row_43_indicator", + "source_label": "$35,001 - $36,000" + }, + { + "id": "row_44", + "kind": "positive", + "upper_cent": 3700000, + "indicator": "taxable_income_age_table_band_row_44_indicator", + "source_label": "$36,001 - $37,000" + }, + { + "id": "row_45", + "kind": "positive", + "upper_cent": 3800000, + "indicator": "taxable_income_age_table_band_row_45_indicator", + "source_label": "$37,001 - $38,000" + }, + { + "id": "row_46", + "kind": "positive", + "upper_cent": 3900000, + "indicator": "taxable_income_age_table_band_row_46_indicator", + "source_label": "$38,001 - $39,000" + }, + { + "id": "row_47", + "kind": "positive", + "upper_cent": 4000000, + "indicator": "taxable_income_age_table_band_row_47_indicator", + "source_label": "$39,001 - $40,000" + }, + { + "id": "row_48", + "kind": "positive", + "upper_cent": 4100000, + "indicator": "taxable_income_age_table_band_row_48_indicator", + "source_label": "$40,001 - $41,000" + }, + { + "id": "row_49", + "kind": "positive", + "upper_cent": 4200000, + "indicator": "taxable_income_age_table_band_row_49_indicator", + "source_label": "$41,001 - $42,000" + }, + { + "id": "row_50", + "kind": "positive", + "upper_cent": 4300000, + "indicator": "taxable_income_age_table_band_row_50_indicator", + "source_label": "$42,001 - $43,000" + }, + { + "id": "row_51", + "kind": "positive", + "upper_cent": 4400000, + "indicator": "taxable_income_age_table_band_row_51_indicator", + "source_label": "$43,001 - $44,000" + }, + { + "id": "row_52", + "kind": "positive", + "upper_cent": 4500000, + "indicator": "taxable_income_age_table_band_row_52_indicator", + "source_label": "$44,001 - $45,000" + }, + { + "id": "row_53", + "kind": "positive", + "upper_cent": 4600000, + "indicator": "taxable_income_age_table_band_row_53_indicator", + "source_label": "$45,001 - $46,000" + }, + { + "id": "row_54", + "kind": "positive", + "upper_cent": 4700000, + "indicator": "taxable_income_age_table_band_row_54_indicator", + "source_label": "$46,001 - $47,000" + }, + { + "id": "row_55", + "kind": "positive", + "upper_cent": 4800000, + "indicator": "taxable_income_age_table_band_row_55_indicator", + "source_label": "$47,001 - $48,000" + }, + { + "id": "row_56", + "kind": "positive", + "upper_cent": 4900000, + "indicator": "taxable_income_age_table_band_row_56_indicator", + "source_label": "$48,001 - $49,000" + }, + { + "id": "row_57", + "kind": "positive", + "upper_cent": 5000000, + "indicator": "taxable_income_age_table_band_row_57_indicator", + "source_label": "$49,001 - $50,000" + }, + { + "id": "row_58", + "kind": "positive", + "upper_cent": 5100000, + "indicator": "taxable_income_age_table_band_row_58_indicator", + "source_label": "$50,000 - $51,000" + }, + { + "id": "row_59", + "kind": "positive", + "upper_cent": 5200000, + "indicator": "taxable_income_age_table_band_row_59_indicator", + "source_label": "$51,001 - $52,000" + }, + { + "id": "row_60", + "kind": "positive", + "upper_cent": 5300000, + "indicator": "taxable_income_age_table_band_row_60_indicator", + "source_label": "$52,001 - $53,000" + }, + { + "id": "row_61", + "kind": "positive", + "upper_cent": 5400000, + "indicator": "taxable_income_age_table_band_row_61_indicator", + "source_label": "$53,001 - $54,000" + }, + { + "id": "row_62", + "kind": "positive", + "upper_cent": 5500000, + "indicator": "taxable_income_age_table_band_row_62_indicator", + "source_label": "$54,001 - $55,000" + }, + { + "id": "row_63", + "kind": "positive", + "upper_cent": 5600000, + "indicator": "taxable_income_age_table_band_row_63_indicator", + "source_label": "$55,001 - $56,000" + }, + { + "id": "row_64", + "kind": "positive", + "upper_cent": 5700000, + "indicator": "taxable_income_age_table_band_row_64_indicator", + "source_label": "$56,001 - $57,000" + }, + { + "id": "row_65", + "kind": "positive", + "upper_cent": 5800000, + "indicator": "taxable_income_age_table_band_row_65_indicator", + "source_label": "$57,001 - $58,000" + }, + { + "id": "row_66", + "kind": "positive", + "upper_cent": 5900000, + "indicator": "taxable_income_age_table_band_row_66_indicator", + "source_label": "$58,001 - $59,000" + }, + { + "id": "row_67", + "kind": "positive", + "upper_cent": 6000000, + "indicator": "taxable_income_age_table_band_row_67_indicator", + "source_label": "$59,001 - $60,000" + }, + { + "id": "row_68", + "kind": "positive", + "upper_cent": 6100000, + "indicator": "taxable_income_age_table_band_row_68_indicator", + "source_label": "$60,001 - $61,000" + }, + { + "id": "row_69", + "kind": "positive", + "upper_cent": 6200000, + "indicator": "taxable_income_age_table_band_row_69_indicator", + "source_label": "$61,001 - $62,000" + }, + { + "id": "row_70", + "kind": "positive", + "upper_cent": 6300000, + "indicator": "taxable_income_age_table_band_row_70_indicator", + "source_label": "$62,001 - $63,000" + }, + { + "id": "row_71", + "kind": "positive", + "upper_cent": 6400000, + "indicator": "taxable_income_age_table_band_row_71_indicator", + "source_label": "$63,001 - $64,000" + }, + { + "id": "row_72", + "kind": "positive", + "upper_cent": 6500000, + "indicator": "taxable_income_age_table_band_row_72_indicator", + "source_label": "$64,001 - $65,000" + }, + { + "id": "row_73", + "kind": "positive", + "upper_cent": 6600000, + "indicator": "taxable_income_age_table_band_row_73_indicator", + "source_label": "$65,001 - $66,000" + }, + { + "id": "row_74", + "kind": "positive", + "upper_cent": 6700000, + "indicator": "taxable_income_age_table_band_row_74_indicator", + "source_label": "$66,001 - $67,000" + }, + { + "id": "row_75", + "kind": "positive", + "upper_cent": 6800000, + "indicator": "taxable_income_age_table_band_row_75_indicator", + "source_label": "$67,001 - $68,000" + }, + { + "id": "row_76", + "kind": "positive", + "upper_cent": 6900000, + "indicator": "taxable_income_age_table_band_row_76_indicator", + "source_label": "$68,001 - $69,000" + }, + { + "id": "row_77", + "kind": "positive", + "upper_cent": 7000000, + "indicator": "taxable_income_age_table_band_row_77_indicator", + "source_label": "$69,001 - $70,000" + }, + { + "id": "row_78", + "kind": "positive", + "upper_cent": 7100000, + "indicator": "taxable_income_age_table_band_row_78_indicator", + "source_label": "$70,001 - $71,000" + }, + { + "id": "row_79", + "kind": "positive", + "upper_cent": 7200000, + "indicator": "taxable_income_age_table_band_row_79_indicator", + "source_label": "$71,001 - $72,000" + }, + { + "id": "row_80", + "kind": "positive", + "upper_cent": 7300000, + "indicator": "taxable_income_age_table_band_row_80_indicator", + "source_label": "$72,001 - $73,000" + }, + { + "id": "row_81", + "kind": "positive", + "upper_cent": 7400000, + "indicator": "taxable_income_age_table_band_row_81_indicator", + "source_label": "$73,001 - $74,000" + }, + { + "id": "row_82", + "kind": "positive", + "upper_cent": 7500000, + "indicator": "taxable_income_age_table_band_row_82_indicator", + "source_label": "$74,001 - $75,000" + }, + { + "id": "row_83", + "kind": "positive", + "upper_cent": 7600000, + "indicator": "taxable_income_age_table_band_row_83_indicator", + "source_label": "$75,001 - $76,000" + }, + { + "id": "row_84", + "kind": "positive", + "upper_cent": 7700000, + "indicator": "taxable_income_age_table_band_row_84_indicator", + "source_label": "$76,001 - $77,000" + }, + { + "id": "row_85", + "kind": "positive", + "upper_cent": 7800000, + "indicator": "taxable_income_age_table_band_row_85_indicator", + "source_label": "$77,001 - $78,000" + }, + { + "id": "row_86", + "kind": "positive", + "upper_cent": 7900000, + "indicator": "taxable_income_age_table_band_row_86_indicator", + "source_label": "$78,001 - $79,000" + }, + { + "id": "row_87", + "kind": "positive", + "upper_cent": 8000000, + "indicator": "taxable_income_age_table_band_row_87_indicator", + "source_label": "$79,001 - $80,000" + }, + { + "id": "row_88", + "kind": "positive", + "upper_cent": 8100000, + "indicator": "taxable_income_age_table_band_row_88_indicator", + "source_label": "$80,000 - $81,000" + }, + { + "id": "row_89", + "kind": "positive", + "upper_cent": 8200000, + "indicator": "taxable_income_age_table_band_row_89_indicator", + "source_label": "$81,001 - $82,000" + }, + { + "id": "row_90", + "kind": "positive", + "upper_cent": 8300000, + "indicator": "taxable_income_age_table_band_row_90_indicator", + "source_label": "$82,001 - $83,000" + }, + { + "id": "row_91", + "kind": "positive", + "upper_cent": 8400000, + "indicator": "taxable_income_age_table_band_row_91_indicator", + "source_label": "$83,001 - $84,000" + }, + { + "id": "row_92", + "kind": "positive", + "upper_cent": 8500000, + "indicator": "taxable_income_age_table_band_row_92_indicator", + "source_label": "$84,001 - $85,000" + }, + { + "id": "row_93", + "kind": "positive", + "upper_cent": 8600000, + "indicator": "taxable_income_age_table_band_row_93_indicator", + "source_label": "$85,001 - $86,000" + }, + { + "id": "row_94", + "kind": "positive", + "upper_cent": 8700000, + "indicator": "taxable_income_age_table_band_row_94_indicator", + "source_label": "$86,001 - $87,000" + }, + { + "id": "row_95", + "kind": "positive", + "upper_cent": 8800000, + "indicator": "taxable_income_age_table_band_row_95_indicator", + "source_label": "$87,001 - $88,000" + }, + { + "id": "row_96", + "kind": "positive", + "upper_cent": 8900000, + "indicator": "taxable_income_age_table_band_row_96_indicator", + "source_label": "$88,001 - $89,000" + }, + { + "id": "row_97", + "kind": "positive", + "upper_cent": 9000000, + "indicator": "taxable_income_age_table_band_row_97_indicator", + "source_label": "$89,001 - $90,000" + }, + { + "id": "row_98", + "kind": "positive", + "upper_cent": 9100000, + "indicator": "taxable_income_age_table_band_row_98_indicator", + "source_label": "$90,001 - $91,000" + }, + { + "id": "row_99", + "kind": "positive", + "upper_cent": 9200000, + "indicator": "taxable_income_age_table_band_row_99_indicator", + "source_label": "$91,001 - $92,000" + }, + { + "id": "row_100", + "kind": "positive", + "upper_cent": 9300000, + "indicator": "taxable_income_age_table_band_row_100_indicator", + "source_label": "$92,001 - $93,000" + }, + { + "id": "row_101", + "kind": "positive", + "upper_cent": 9400000, + "indicator": "taxable_income_age_table_band_row_101_indicator", + "source_label": "$93,001 - $94,000" + }, + { + "id": "row_102", + "kind": "positive", + "upper_cent": 9500000, + "indicator": "taxable_income_age_table_band_row_102_indicator", + "source_label": "$94,001 - $95,000" + }, + { + "id": "row_103", + "kind": "positive", + "upper_cent": 9600000, + "indicator": "taxable_income_age_table_band_row_103_indicator", + "source_label": "$95,001 - $96,000" + }, + { + "id": "row_104", + "kind": "positive", + "upper_cent": 9700000, + "indicator": "taxable_income_age_table_band_row_104_indicator", + "source_label": "$96,001 - $97,000" + }, + { + "id": "row_105", + "kind": "positive", + "upper_cent": 9800000, + "indicator": "taxable_income_age_table_band_row_105_indicator", + "source_label": "$97,001 - $98,000" + }, + { + "id": "row_106", + "kind": "positive", + "upper_cent": 9900000, + "indicator": "taxable_income_age_table_band_row_106_indicator", + "source_label": "$98,001 - $99,000" + }, + { + "id": "row_107", + "kind": "positive", + "upper_cent": 10000000, + "indicator": "taxable_income_age_table_band_row_107_indicator", + "source_label": "$99,001 - $100,000" + }, + { + "id": "row_108", + "kind": "positive", + "upper_cent": 10100000, + "indicator": "taxable_income_age_table_band_row_108_indicator", + "source_label": "$100,001 - $101,000" + }, + { + "id": "row_109", + "kind": "positive", + "upper_cent": 10200000, + "indicator": "taxable_income_age_table_band_row_109_indicator", + "source_label": "$101,001 - $102,000" + }, + { + "id": "row_110", + "kind": "positive", + "upper_cent": 10300000, + "indicator": "taxable_income_age_table_band_row_110_indicator", + "source_label": "$102,001 - $103,000" + }, + { + "id": "row_111", + "kind": "positive", + "upper_cent": 10400000, + "indicator": "taxable_income_age_table_band_row_111_indicator", + "source_label": "$103,001 - $104,000" + }, + { + "id": "row_112", + "kind": "positive", + "upper_cent": 10500000, + "indicator": "taxable_income_age_table_band_row_112_indicator", + "source_label": "$104,001 - $105,000" + }, + { + "id": "row_113", + "kind": "positive", + "upper_cent": 10600000, + "indicator": "taxable_income_age_table_band_row_113_indicator", + "source_label": "$105,001 - $106,000" + }, + { + "id": "row_114", + "kind": "positive", + "upper_cent": 10700000, + "indicator": "taxable_income_age_table_band_row_114_indicator", + "source_label": "$106,001 - $107,000" + }, + { + "id": "row_115", + "kind": "positive", + "upper_cent": 10800000, + "indicator": "taxable_income_age_table_band_row_115_indicator", + "source_label": "$107,001 - $108,000" + }, + { + "id": "row_116", + "kind": "positive", + "upper_cent": 10900000, + "indicator": "taxable_income_age_table_band_row_116_indicator", + "source_label": "$108,001 - $109,000" + }, + { + "id": "row_117", + "kind": "positive", + "upper_cent": 11000000, + "indicator": "taxable_income_age_table_band_row_117_indicator", + "source_label": "$109,001 - $110,000" + }, + { + "id": "row_118", + "kind": "positive", + "upper_cent": 11100000, + "indicator": "taxable_income_age_table_band_row_118_indicator", + "source_label": "$110,001 - $111,000" + }, + { + "id": "row_119", + "kind": "positive", + "upper_cent": 11200000, + "indicator": "taxable_income_age_table_band_row_119_indicator", + "source_label": "$111,001 - $112,000" + }, + { + "id": "row_120", + "kind": "positive", + "upper_cent": 11300000, + "indicator": "taxable_income_age_table_band_row_120_indicator", + "source_label": "$112,001 - $113,000" + }, + { + "id": "row_121", + "kind": "positive", + "upper_cent": 11400000, + "indicator": "taxable_income_age_table_band_row_121_indicator", + "source_label": "$113,001 - $114,000" + }, + { + "id": "row_122", + "kind": "positive", + "upper_cent": 11500000, + "indicator": "taxable_income_age_table_band_row_122_indicator", + "source_label": "$114,001 - $115,000" + }, + { + "id": "row_123", + "kind": "positive", + "upper_cent": 11600000, + "indicator": "taxable_income_age_table_band_row_123_indicator", + "source_label": "$115,001 - $116,000" + }, + { + "id": "row_124", + "kind": "positive", + "upper_cent": 11700000, + "indicator": "taxable_income_age_table_band_row_124_indicator", + "source_label": "$116,001 - $117,000" + }, + { + "id": "row_125", + "kind": "positive", + "upper_cent": 11800000, + "indicator": "taxable_income_age_table_band_row_125_indicator", + "source_label": "$117,001 - $118,000" + }, + { + "id": "row_126", + "kind": "positive", + "upper_cent": 11900000, + "indicator": "taxable_income_age_table_band_row_126_indicator", + "source_label": "$118,001 - $119,000" + }, + { + "id": "row_127", + "kind": "positive", + "upper_cent": 12000000, + "indicator": "taxable_income_age_table_band_row_127_indicator", + "source_label": "$119,001 - $120,000" + }, + { + "id": "row_128", + "kind": "positive", + "upper_cent": 12100000, + "indicator": "taxable_income_age_table_band_row_128_indicator", + "source_label": "$120,001 - $121,000" + }, + { + "id": "row_129", + "kind": "positive", + "upper_cent": 12200000, + "indicator": "taxable_income_age_table_band_row_129_indicator", + "source_label": "$121,001 - $122,000" + }, + { + "id": "row_130", + "kind": "positive", + "upper_cent": 12300000, + "indicator": "taxable_income_age_table_band_row_130_indicator", + "source_label": "$122,001 - $123,000" + }, + { + "id": "row_131", + "kind": "positive", + "upper_cent": 12400000, + "indicator": "taxable_income_age_table_band_row_131_indicator", + "source_label": "$123,001 - $124,000" + }, + { + "id": "row_132", + "kind": "positive", + "upper_cent": 12500000, + "indicator": "taxable_income_age_table_band_row_132_indicator", + "source_label": "$124,001 - $125,000" + }, + { + "id": "row_133", + "kind": "positive", + "upper_cent": 12600000, + "indicator": "taxable_income_age_table_band_row_133_indicator", + "source_label": "$125,001 - $126,000" + }, + { + "id": "row_134", + "kind": "positive", + "upper_cent": 12700000, + "indicator": "taxable_income_age_table_band_row_134_indicator", + "source_label": "$126,001 - $127,000" + }, + { + "id": "row_135", + "kind": "positive", + "upper_cent": 12800000, + "indicator": "taxable_income_age_table_band_row_135_indicator", + "source_label": "$127,001 - $128,000" + }, + { + "id": "row_136", + "kind": "positive", + "upper_cent": 12900000, + "indicator": "taxable_income_age_table_band_row_136_indicator", + "source_label": "$128,001 - $129,000" + }, + { + "id": "row_137", + "kind": "positive", + "upper_cent": 13000000, + "indicator": "taxable_income_age_table_band_row_137_indicator", + "source_label": "$129,001 - $130,000" + }, + { + "id": "row_138", + "kind": "positive", + "upper_cent": 13100000, + "indicator": "taxable_income_age_table_band_row_138_indicator", + "source_label": "$130,001 - $131,000" + }, + { + "id": "row_139", + "kind": "positive", + "upper_cent": 13200000, + "indicator": "taxable_income_age_table_band_row_139_indicator", + "source_label": "$131,001 - $132,000" + }, + { + "id": "row_140", + "kind": "positive", + "upper_cent": 13300000, + "indicator": "taxable_income_age_table_band_row_140_indicator", + "source_label": "$132,001 - $133,000" + }, + { + "id": "row_141", + "kind": "positive", + "upper_cent": 13400000, + "indicator": "taxable_income_age_table_band_row_141_indicator", + "source_label": "$133,001 - $134,000" + }, + { + "id": "row_142", + "kind": "positive", + "upper_cent": 13500000, + "indicator": "taxable_income_age_table_band_row_142_indicator", + "source_label": "$134,001 - $135,000" + }, + { + "id": "row_143", + "kind": "positive", + "upper_cent": 13600000, + "indicator": "taxable_income_age_table_band_row_143_indicator", + "source_label": "$135,001 - $136,000" + }, + { + "id": "row_144", + "kind": "positive", + "upper_cent": 13700000, + "indicator": "taxable_income_age_table_band_row_144_indicator", + "source_label": "$136,001 - $137,000" + }, + { + "id": "row_145", + "kind": "positive", + "upper_cent": 13800000, + "indicator": "taxable_income_age_table_band_row_145_indicator", + "source_label": "$137,001 - $138,000" + }, + { + "id": "row_146", + "kind": "positive", + "upper_cent": 13900000, + "indicator": "taxable_income_age_table_band_row_146_indicator", + "source_label": "$138,001 - $139,000" + }, + { + "id": "row_147", + "kind": "positive", + "upper_cent": 14000000, + "indicator": "taxable_income_age_table_band_row_147_indicator", + "source_label": "$139,001 - $140,000" + }, + { + "id": "row_148", + "kind": "positive", + "upper_cent": 14100000, + "indicator": "taxable_income_age_table_band_row_148_indicator", + "source_label": "$140,001 - $141,000" + }, + { + "id": "row_149", + "kind": "positive", + "upper_cent": 14200000, + "indicator": "taxable_income_age_table_band_row_149_indicator", + "source_label": "$141,001 - $142,000" + }, + { + "id": "row_150", + "kind": "positive", + "upper_cent": 14300000, + "indicator": "taxable_income_age_table_band_row_150_indicator", + "source_label": "$142,001 - $143,000" + }, + { + "id": "row_151", + "kind": "positive", + "upper_cent": 14400000, + "indicator": "taxable_income_age_table_band_row_151_indicator", + "source_label": "$143,001 - $144,000" + }, + { + "id": "row_152", + "kind": "positive", + "upper_cent": 14500000, + "indicator": "taxable_income_age_table_band_row_152_indicator", + "source_label": "$144,001 - $145,000" + }, + { + "id": "row_153", + "kind": "positive", + "upper_cent": 14600000, + "indicator": "taxable_income_age_table_band_row_153_indicator", + "source_label": "$145,001 - $146,000" + }, + { + "id": "row_154", + "kind": "positive", + "upper_cent": 14700000, + "indicator": "taxable_income_age_table_band_row_154_indicator", + "source_label": "$146,001 - $147,000" + }, + { + "id": "row_155", + "kind": "positive", + "upper_cent": 14800000, + "indicator": "taxable_income_age_table_band_row_155_indicator", + "source_label": "$147,001 - $148,000" + }, + { + "id": "row_156", + "kind": "positive", + "upper_cent": 14900000, + "indicator": "taxable_income_age_table_band_row_156_indicator", + "source_label": "$148,001 - $149,000" + }, + { + "id": "row_157", + "kind": "positive", + "upper_cent": 15000000, + "indicator": "taxable_income_age_table_band_row_157_indicator", + "source_label": "$149,001 - $150,000" + }, + { + "id": "row_158", + "kind": "positive", + "upper_cent": 15100000, + "indicator": "taxable_income_age_table_band_row_158_indicator", + "source_label": "$150,001 - $151,000" + }, + { + "id": "row_159", + "kind": "positive", + "upper_cent": 15200000, + "indicator": "taxable_income_age_table_band_row_159_indicator", + "source_label": "$151,001 - $152,000" + }, + { + "id": "row_160", + "kind": "positive", + "upper_cent": 15300000, + "indicator": "taxable_income_age_table_band_row_160_indicator", + "source_label": "$152,001 - $153,000" + }, + { + "id": "row_161", + "kind": "positive", + "upper_cent": 15400000, + "indicator": "taxable_income_age_table_band_row_161_indicator", + "source_label": "$153,001 - $154,000" + }, + { + "id": "row_162", + "kind": "positive", + "upper_cent": 15500000, + "indicator": "taxable_income_age_table_band_row_162_indicator", + "source_label": "$154,001 - $155,000" + }, + { + "id": "row_163", + "kind": "positive", + "upper_cent": 15600000, + "indicator": "taxable_income_age_table_band_row_163_indicator", + "source_label": "$155,001 - $156,000" + }, + { + "id": "row_164", + "kind": "positive", + "upper_cent": 15700000, + "indicator": "taxable_income_age_table_band_row_164_indicator", + "source_label": "$156,001 - $157,000" + }, + { + "id": "row_165", + "kind": "positive", + "upper_cent": 15800000, + "indicator": "taxable_income_age_table_band_row_165_indicator", + "source_label": "$157,001 - $158,000" + }, + { + "id": "row_166", + "kind": "positive", + "upper_cent": 15900000, + "indicator": "taxable_income_age_table_band_row_166_indicator", + "source_label": "$158,001 - $159,000" + }, + { + "id": "row_167", + "kind": "positive", + "upper_cent": 16000000, + "indicator": "taxable_income_age_table_band_row_167_indicator", + "source_label": "$159,001 - $160,000" + }, + { + "id": "row_168", + "kind": "positive", + "upper_cent": 16100000, + "indicator": "taxable_income_age_table_band_row_168_indicator", + "source_label": "$160,001 - $161,000" + }, + { + "id": "row_169", + "kind": "positive", + "upper_cent": 16200000, + "indicator": "taxable_income_age_table_band_row_169_indicator", + "source_label": "$161,001 - $162,000" + }, + { + "id": "row_170", + "kind": "positive", + "upper_cent": 16300000, + "indicator": "taxable_income_age_table_band_row_170_indicator", + "source_label": "$162,001 - $163,000" + }, + { + "id": "row_171", + "kind": "positive", + "upper_cent": 16400000, + "indicator": "taxable_income_age_table_band_row_171_indicator", + "source_label": "$163,001 - $164,000" + }, + { + "id": "row_172", + "kind": "positive", + "upper_cent": 16500000, + "indicator": "taxable_income_age_table_band_row_172_indicator", + "source_label": "$164,001 - $165,000" + }, + { + "id": "row_173", + "kind": "positive", + "upper_cent": 16600000, + "indicator": "taxable_income_age_table_band_row_173_indicator", + "source_label": "$165,001 - $166,000" + }, + { + "id": "row_174", + "kind": "positive", + "upper_cent": 16700000, + "indicator": "taxable_income_age_table_band_row_174_indicator", + "source_label": "$166,001 - $167,000" + }, + { + "id": "row_175", + "kind": "positive", + "upper_cent": 16800000, + "indicator": "taxable_income_age_table_band_row_175_indicator", + "source_label": "$167,001 - $168,000" + }, + { + "id": "row_176", + "kind": "positive", + "upper_cent": 16900000, + "indicator": "taxable_income_age_table_band_row_176_indicator", + "source_label": "$168,001 - $169,000" + }, + { + "id": "row_177", + "kind": "positive", + "upper_cent": 17000000, + "indicator": "taxable_income_age_table_band_row_177_indicator", + "source_label": "$169,001 - $170,000" + }, + { + "id": "row_178", + "kind": "positive", + "upper_cent": 17100000, + "indicator": "taxable_income_age_table_band_row_178_indicator", + "source_label": "$170,001 - $171,000" + }, + { + "id": "row_179", + "kind": "positive", + "upper_cent": 17200000, + "indicator": "taxable_income_age_table_band_row_179_indicator", + "source_label": "$171,001 - $172,000" + }, + { + "id": "row_180", + "kind": "positive", + "upper_cent": 17300000, + "indicator": "taxable_income_age_table_band_row_180_indicator", + "source_label": "$172,001 - $173,000" + }, + { + "id": "row_181", + "kind": "positive", + "upper_cent": 17400000, + "indicator": "taxable_income_age_table_band_row_181_indicator", + "source_label": "$173,001 - $174,000" + }, + { + "id": "row_182", + "kind": "positive", + "upper_cent": 17500000, + "indicator": "taxable_income_age_table_band_row_182_indicator", + "source_label": "$174,001 - $175,000" + }, + { + "id": "row_183", + "kind": "positive", + "upper_cent": 17600000, + "indicator": "taxable_income_age_table_band_row_183_indicator", + "source_label": "$175,001 - $176,000" + }, + { + "id": "row_184", + "kind": "positive", + "upper_cent": 17700000, + "indicator": "taxable_income_age_table_band_row_184_indicator", + "source_label": "$176,001 - $177,000" + }, + { + "id": "row_185", + "kind": "positive", + "upper_cent": 17800000, + "indicator": "taxable_income_age_table_band_row_185_indicator", + "source_label": "$177,001 - $178,000" + }, + { + "id": "row_186", + "kind": "positive", + "upper_cent": 17900000, + "indicator": "taxable_income_age_table_band_row_186_indicator", + "source_label": "$178,001 - $179,000" + }, + { + "id": "row_187", + "kind": "positive", + "upper_cent": 18000000, + "indicator": "taxable_income_age_table_band_row_187_indicator", + "source_label": "$179,001 - $180,000" + }, + { + "id": "row_188", + "kind": "positive", + "upper_cent": null, + "indicator": "taxable_income_age_table_band_row_188_indicator", + "source_label": "Over $180,000" + } + ], + "description": "Age-by-income TY2024 taxable-income bands, rows 6-188: one nil band, 181 finite bands and the open tail over $180,000. Declared and validated only; no reference companions and no fit rows." + } + ] +} diff --git a/packages/microcosm-build/src/microcosm/build/nz/reference_profiles.json b/packages/microcosm-build/src/microcosm/build/nz/reference_profiles.json index 4c891ca5a7..a00eaf9637 100644 --- a/packages/microcosm-build/src/microcosm/build/nz/reference_profiles.json +++ b/packages/microcosm-build/src/microcosm/build/nz/reference_profiles.json @@ -1,6 +1,7 @@ { "schema_version": 1, "factor_context_resource": "reference_aging.json", + "interval_resource": "reference_intervals.json", "dimensions": { "census_residual_allocation": { "default": "mar", @@ -71085,7 +71086,9 @@ "ird_taxable_income_people_by_band__row_243", "ird_taxable_income_people_by_band__row_244" ], - "interval_table_ids": [], + "interval_table_ids": [ + "ird_taxable_income_national_ty2024" + ], "required_measurement_binding": "ird_taxable_income_ty2027_v1" } ] @@ -71573,7 +71576,9 @@ "ird_taxable_income_people_by_band__row_243", "ird_taxable_income_people_by_band__row_244" ], - "interval_table_ids": [], + "interval_table_ids": [ + "ird_taxable_income_national_ty2024" + ], "required_measurement_binding": "ird_taxable_income_ty2027_v1" } ] @@ -72061,7 +72066,9 @@ "ird_taxable_income_people_by_band__row_243", "ird_taxable_income_people_by_band__row_244" ], - "interval_table_ids": [], + "interval_table_ids": [ + "ird_taxable_income_national_ty2024" + ], "required_measurement_binding": "ird_taxable_income_ty2027_v1" } ] diff --git a/packages/microcosm-build/src/microcosm/build/reference_intervals.py b/packages/microcosm-build/src/microcosm/build/reference_intervals.py new file mode 100644 index 0000000000..61652b5689 --- /dev/null +++ b/packages/microcosm-build/src/microcosm/build/reference_intervals.py @@ -0,0 +1,981 @@ +"""Exact income-band intervals with declared semantics and rational cuts. + +Country data declares every interval table: its entity, currency, input unit, +income and universe columns, closure, nil band, negative-income policy, first +lower cut, source locator and diagnostics window. This module supplies no +default for any of them. It validates the declarations, scales cuts exactly, +classifies signed integer-cent amounts and reports fixed-weight boundary +diagnostics. + +Each cut is stored once: a positive band's lower cut is its predecessor's +upper cut, and the first positive band starts at the declared first lower cut. +Source cuts are integer cents. Scaling by a positive rational factor produces +reduced rational cents through integer cross-products and never rounds a +scaled cut. A scaled table keeps its source cuts and the digest of its source +table, and cannot be scaled again. + +Classification validates every record before computing any band. A universe +status is ``included``, ``excluded`` or ``unresolved``; an unresolved record, +an invalid status or an invalid amount on an included record refuses with a +summary of counts by reason. Excluded records never enter a band. Amounts are +signed integers; booleans, floats, strings, missing and nonfinite values are +invalid, and nothing converts a float to cents. + +Diagnostics weigh records with ``frame.resolve_weights(entity)``. They report +weighted mass near every finite cut and each positive band's weighted count +and amount under the declared closure and the other closure, computed +exactly. They never change a reference value or target. A published histogram +does not identify the publisher's mass exactly on a cut, so publisher +reclassification is unavailable: the report gives only the fixed-population +model measurement change. Records below the first cut remain visible, and +floored negative records retain their signed pre-floor amounts and their +destination under each closure. +""" + +from __future__ import annotations + +import copy +import hashlib +import json +import math +from collections import Counter +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +from fractions import Fraction +from numbers import Integral + +import numpy as np + +from microcosm.graph.canonical import canonical_json + +__all__ = [ + "BELOW_FIRST_CUT", + "EXCLUDED", + "INTERVAL_DOCUMENT_SCHEMA_VERSION", + "IntervalPreparationError", + "IntervalTable", + "alternative_closure", + "boundary_diagnostics", + "classify_records", + "frame_boundary_diagnostics", + "interval_document_sha256", + "interval_indicators", + "interval_input_fingerprint", + "interval_table", + "scale_interval_document", + "scale_interval_table", + "source_interval_table", + "validate_interval_document", +] + +INTERVAL_DOCUMENT_SCHEMA_VERSION = 1 +SCALING_OPERATION = "exact_cut_multiplication" +BELOW_FIRST_CUT = -1 +EXCLUDED = -2 +CLOSURES = ("left", "right") +NIL_BAND_STATES = ("present", "absent") +NEGATIVE_INCOME_POLICIES = ("floor_zero", "refuse") +INPUT_UNITS = ("integer_cent",) +UNIVERSE_STATES = ("included", "excluded", "unresolved") +PUBLISHER_RECLASSIFICATION = "unavailable" +LIMITATION = ( + "A published histogram does not identify the publisher's mass exactly on a " + "cut, so the publisher's reclassification under the alternative closure " + "cannot be recovered. These are fixed-weight model measurement changes; " + "reference values and calibration targets are unchanged." +) + +_DOCUMENT_FIELDS = frozenset( + {"schema_version", "country", "description", "disclosure", "tables"} +) +_TABLE_FIELDS = frozenset( + { + "id", + "entity", + "currency", + "input_unit", + "income_column", + "universe_column", + "source_period", + "target_period", + "positive_band_closure", + "nil_band", + "negative_income_policy", + "first_lower_cut_cents", + "source_locator", + "diagnostics", + "bands", + } +) +_SCALED_TABLE_FIELDS = frozenset({"scaling", "source_first_lower_cut_cents"}) +_OPTIONAL_TABLE_FIELDS = frozenset({"description"}) +_BAND_FIELDS = frozenset({"id", "kind", "indicator"}) +_OPTIONAL_BAND_FIELDS = frozenset( + {"count_reference", "amount_reference", "source_label"} +) +_SCALING_FIELDS = frozenset( + { + "operation", + "factor_id", + "operational_factor", + "factor_declaration_sha256", + "factor_receipt_sha256", + "source_table_sha256", + "source_cuts_sha256", + } +) +_DIAGNOSTIC_FIELDS = frozenset( + {"boundary_window_cents", "alternative_positive_band_closure"} +) + + +class IntervalPreparationError(ValueError): + """Records cannot be classified; ``summary`` counts every reason.""" + + def __init__(self, summary: Mapping[str, object]) -> None: + self.summary = dict(summary) + super().__init__( + "Interval preparation refused before emitting indicators: " + + json.dumps(self.summary, sort_keys=True) + ) + + +@dataclass(frozen=True) +class IntervalTable: + """One validated table. ``cuts`` holds the first lower cut then each finite upper cut.""" + + id: str + entity: str + income_column: str + universe_column: str + closure: str + nil_present: bool + negative_income_policy: str + window_cents: int + band_ids: tuple[str, ...] + indicators: tuple[str, ...] + nil_index: int | None + positive_indices: tuple[int, ...] + cuts: tuple[Fraction, ...] + scaled: bool + declaration: Mapping[str, object] + + +def interval_document_sha256(document: object) -> str: + """Hash canonical document bytes.""" + + return hashlib.sha256(canonical_json(document)).hexdigest() + + +def alternative_closure(closure: str) -> str: + """The other positive-band closure, whichever is declared.""" + + if closure not in CLOSURES: + raise ValueError("Positive band closure must be 'left' or 'right'.") + return "left" if closure == "right" else "right" + + +def _mapping(value: object, label: str) -> dict: + if not isinstance(value, Mapping): + raise ValueError(f"{label} must be an object.") + return dict(value) + + +def _closed(value: object, required: frozenset, optional: frozenset, label: str): + row = _mapping(value, label) + unknown = row.keys() - required - optional + missing = required - row.keys() + if unknown: + raise ValueError(f"Unknown {label} fields: {sorted(unknown)}.") + if missing: + raise ValueError(f"{label} is missing declared fields: {sorted(missing)}.") + return row + + +def _text(value: object, label: str) -> str: + if not isinstance(value, str) or not value: + raise ValueError(f"{label} must be nonempty text.") + return value + + +def _choice(value: object, choices: tuple[str, ...], label: str) -> str: + if not isinstance(value, str) or value not in choices: + raise ValueError(f"{label} must be declared as one of {list(choices)}.") + return value + + +def _period(value: object, label: str): + if isinstance(value, bool) or type(value) not in {int, str}: + raise ValueError(f"{label} must be an integer or nonempty text.") + if isinstance(value, str) and not value: + raise ValueError(f"{label} must be an integer or nonempty text.") + return value + + +def _integer_cents(value: object, label: str) -> int: + # ``type`` rather than ``isinstance`` keeps booleans out; JSON floats, + # infinity and NaN are floats and refuse here too. + if type(value) is not int: + raise ValueError(f"{label} must be integer cents, never a float or boolean.") + return value + + +def _rational_cents(value: object, label: str) -> Fraction: + row = _closed(value, frozenset({"numerator", "denominator"}), frozenset(), label) + numerator, denominator = row["numerator"], row["denominator"] + if type(numerator) is not int or type(denominator) is not int or denominator <= 0: + raise ValueError( + f"{label} requires integer numerator and positive integer denominator." + ) + if math.gcd(numerator, denominator) != 1: + raise ValueError(f"{label} must be a reduced rational.") + return Fraction(numerator, denominator) + + +def _rational(value: Fraction) -> dict[str, int]: + return {"numerator": value.numerator, "denominator": value.denominator} + + +def _positive_factor(value: object) -> Fraction: + row = _closed( + value, frozenset({"numerator", "denominator"}), frozenset(), "scaling factor" + ) + numerator, denominator = row["numerator"], row["denominator"] + if ( + type(numerator) is not int + or type(denominator) is not int + or numerator <= 0 + or denominator <= 0 + ): + raise ValueError("A scaling factor must be a positive integer rational.") + return Fraction(numerator, denominator) + + +def _source_table(table: Mapping) -> dict: + """Reconstruct the source declaration a scaled table was derived from.""" + + source = { + key: copy.deepcopy(value) + for key, value in table.items() + if key not in _SCALED_TABLE_FIELDS + } + source["first_lower_cut_cents"] = table["source_first_lower_cut_cents"] + bands = [] + for band in table["bands"]: + row = {key: copy.deepcopy(value) for key, value in band.items()} + if "source_upper_cent" in row: + row["upper_cent"] = row.pop("source_upper_cent") + bands.append(row) + source["bands"] = bands + return source + + +def _source_cuts(source: Mapping) -> dict: + return { + "first_lower_cut_cents": source["first_lower_cut_cents"], + "upper_cents": [ + band["upper_cent"] for band in source["bands"] if band["kind"] == "positive" + ], + } + + +def _cut_payload(table: IntervalTable) -> dict: + return { + "first_lower_cut_cents": _rational(table.cuts[0]), + "upper_cents": [_rational(cut) for cut in table.cuts[1:]] + [None], + } + + +def interval_table(value: object) -> IntervalTable: + """Validate one source or scaled table declaration.""" + + raw = _mapping(value, "interval table") + scaled = "scaling" in raw + required = _TABLE_FIELDS | (_SCALED_TABLE_FIELDS if scaled else frozenset()) + row = _closed(raw, required, _OPTIONAL_TABLE_FIELDS, "interval table") + table_id = _text(row["id"], "Interval table id") + entity = _text(row["entity"], "Interval table entity") + _text(row["currency"], "Interval table currency") + _choice(row["input_unit"], INPUT_UNITS, "Interval table input_unit") + income = _text(row["income_column"], "Interval income column") + universe = _text(row["universe_column"], "Interval universe column") + if income == universe: + raise ValueError("Interval income and universe columns must differ.") + _period(row["source_period"], "Interval source period") + _period(row["target_period"], "Interval target period") + closure = _choice( + row["positive_band_closure"], CLOSURES, "Interval positive_band_closure" + ) + nil_present = ( + _choice(row["nil_band"], NIL_BAND_STATES, "Interval nil_band") == "present" + ) + policy = _choice( + row["negative_income_policy"], + NEGATIVE_INCOME_POLICIES, + "Interval negative_income_policy", + ) + _text(row["source_locator"], "Interval source locator") + if "description" in row: + _text(row["description"], "Interval table description") + diagnostics = _closed( + row["diagnostics"], _DIAGNOSTIC_FIELDS, frozenset(), "interval diagnostics" + ) + window = diagnostics["boundary_window_cents"] + if type(window) is not int or window < 0: + raise ValueError("boundary_window_cents must be a nonnegative integer.") + if diagnostics["alternative_positive_band_closure"] != alternative_closure(closure): + raise ValueError( + "The diagnostic alternative must be the other positive band closure." + ) + bands = row["bands"] + if not isinstance(bands, list) or not bands: + raise ValueError("An interval table needs a nonempty band list.") + if scaled: + first = _rational_cents(row["first_lower_cut_cents"], "Scaled first lower cut") + else: + first = Fraction( + _integer_cents(row["first_lower_cut_cents"], "First lower cut") + ) + if first < 0: + raise ValueError("The first lower cut must be nonnegative.") + if nil_present and closure == "left" and first == 0: + raise ValueError( + "A left-closed table with a nil band requires a positive first lower " + "cut; zero cannot belong to both nil and a positive band." + ) + band_ids, indicators, cuts, positive = [], [], [first], [] + nil_index, open_tail = None, False + companions = set() + for index, raw_band in enumerate(bands): + optional = _OPTIONAL_BAND_FIELDS + if scaled: + optional = optional | {"source_upper_cent"} + band = _mapping(raw_band, "interval band") + kind = band.get("kind") + required_band = _BAND_FIELDS | ( + {"upper_cent"} if kind == "positive" else frozenset() + ) + band = _closed(band, frozenset(required_band), optional, "interval band") + band_ids.append(_text(band["id"], "Interval band id")) + indicators.append(_text(band["indicator"], "Interval band indicator")) + if "source_label" in band: + _text(band["source_label"], "Interval band source label") + has_companions = tuple( + key in band for key in ("count_reference", "amount_reference") + ) + if has_companions not in {(True, True), (False, False)}: + raise ValueError("A band names both companion references or neither.") + companions.add(has_companions[0]) + if has_companions[0]: + _text(band["count_reference"], "Band count reference") + _text(band["amount_reference"], "Band amount reference") + if band["count_reference"] == band["amount_reference"]: + raise ValueError("A band's count and amount references must differ.") + if kind == "nil": + if not nil_present: + raise ValueError("A table declaring nil_band absent has a nil band.") + if index != 0: + raise ValueError("A nil band, when present, comes first.") + if "source_upper_cent" in band: + raise ValueError("A nil band has no cut.") + nil_index = index + continue + if kind != "positive": + raise ValueError("Interval band kind must be 'nil' or 'positive'.") + if open_tail: + raise ValueError("Exactly one final open tail ends the band list.") + positive.append(index) + upper = band["upper_cent"] + if upper is None: + open_tail = True + if scaled and band.get("source_upper_cent", 0) is not None: + raise ValueError("A scaled open tail keeps a null source cut.") + continue + if scaled: + value = _rational_cents(upper, "Scaled upper cut") + if "source_upper_cent" not in band: + raise ValueError("A scaled band keeps its source cut.") + else: + value = Fraction(_integer_cents(upper, "Upper cut")) + if value <= 0 or value <= cuts[-1]: + raise ValueError("Upper cuts must be positive and strictly increasing.") + cuts.append(value) + if nil_present and nil_index is None: + raise ValueError("A table declaring nil_band present needs a first nil band.") + if not open_tail: + raise ValueError( + "Exactly one final open tail with upper_cent null is required." + ) + if not positive: + raise ValueError("An interval table needs positive bands.") + if len(companions) != 1: + raise ValueError( + "A table's bands either all name companion references or none do." + ) + if len(set(band_ids)) != len(band_ids): + raise ValueError("Interval band ids must be unique within a table.") + if len(set(indicators)) != len(indicators) or {income, universe} & set(indicators): + raise ValueError("Interval band indicators must be unique output columns.") + table = IntervalTable( + id=table_id, + entity=entity, + income_column=income, + universe_column=universe, + closure=closure, + nil_present=nil_present, + negative_income_policy=policy, + window_cents=window, + band_ids=tuple(band_ids), + indicators=tuple(indicators), + nil_index=nil_index, + positive_indices=tuple(positive), + cuts=tuple(cuts), + scaled=scaled, + declaration=copy.deepcopy(row), + ) + if scaled: + _validate_scaling(table, row) + return table + + +def _validate_scaling(table: IntervalTable, row: Mapping) -> None: + scaling = _closed(row["scaling"], _SCALING_FIELDS, frozenset(), "interval scaling") + if scaling["operation"] != SCALING_OPERATION: + raise ValueError("Unknown interval scaling operation.") + _text(scaling["factor_id"], "Scaling factor id") + factor = _positive_factor(scaling["operational_factor"]) + for key in ( + "factor_declaration_sha256", + "factor_receipt_sha256", + "source_table_sha256", + "source_cuts_sha256", + ): + digest = scaling[key] + if ( + not isinstance(digest, str) + or len(digest) != 64 + or set(digest) - set("0123456789abcdef") + ): + raise ValueError(f"Scaling {key} must be a lowercase SHA-256.") + source = _source_table(row) + parsed = interval_table(source) + if parsed.scaled: + raise ValueError("A scaled table's source cannot itself be scaled.") + if ( + interval_document_sha256(source) != scaling["source_table_sha256"] + or interval_document_sha256(_source_cuts(source)) + != scaling["source_cuts_sha256"] + ): + raise ValueError("Scaled interval source table identity mismatch.") + expected = tuple(cut * factor for cut in parsed.cuts) + if expected != table.cuts: + raise ValueError( + "Scaled interval cuts must equal their source cuts times the factor " + "exactly once." + ) + + +def validate_interval_document(document: object) -> dict[str, IntervalTable]: + """Validate a whole document; return its tables by id in declared order.""" + + row = _closed( + document, + frozenset({"schema_version", "tables"}), + _DOCUMENT_FIELDS - {"schema_version", "tables"}, + "interval document", + ) + if ( + type(row["schema_version"]) is not int + or row["schema_version"] != INTERVAL_DOCUMENT_SCHEMA_VERSION + ): + raise ValueError("Interval documents require schema_version 1.") + for key in ("country", "description", "disclosure"): + if key in row: + _text(row[key], f"Interval document {key}") + tables = row["tables"] + if not isinstance(tables, list) or not tables: + raise ValueError("An interval document needs a nonempty table list.") + result: dict[str, IntervalTable] = {} + outputs, references = set(), [] + for raw in tables: + table = interval_table(raw) + if table.id in result: + raise ValueError("Interval table ids must be unique.") + result[table.id] = table + if outputs & set(table.indicators): + raise ValueError("Interval indicator ids must be unique.") + outputs.update(table.indicators) + for band in table.declaration["bands"]: + references.extend( + band[key] + for key in ("count_reference", "amount_reference") + if key in band + ) + if len(set(references)) != len(references): + raise ValueError("A companion reference belongs to at most one band.") + return result + + +def scale_interval_table( + table: Mapping, + factor: Mapping, + *, + factor_id: str, + factor_declaration_sha256: str, + factor_receipt_sha256: str, +) -> dict: + """Multiply every cut once by a positive rational; never round a cut.""" + + source = interval_table(table) + if source.scaled: + raise ValueError(f"Interval table {source.id!r} is already scaled.") + rational = _positive_factor(factor) + raw = copy.deepcopy(source.declaration) + scaled = { + key: value for key, value in raw.items() if key != "first_lower_cut_cents" + } + scaled["source_first_lower_cut_cents"] = raw["first_lower_cut_cents"] + first = Fraction(raw["first_lower_cut_cents"]) * rational + scaled["first_lower_cut_cents"] = _rational(first) + bands = [] + for band in raw["bands"]: + band = dict(band) + if band["kind"] == "positive": + cut = band["upper_cent"] + band["source_upper_cent"] = cut + band["upper_cent"] = None if cut is None else _rational(cut * rational) + bands.append(band) + scaled["bands"] = bands + scaled["scaling"] = { + "operation": SCALING_OPERATION, + "factor_id": factor_id, + # Recorded as declared (for example p / 10**digits), not reduced. + "operational_factor": { + "numerator": factor["numerator"], + "denominator": factor["denominator"], + }, + "factor_declaration_sha256": factor_declaration_sha256, + "factor_receipt_sha256": factor_receipt_sha256, + "source_table_sha256": interval_document_sha256(raw), + "source_cuts_sha256": interval_document_sha256(_source_cuts(raw)), + } + interval_table(scaled) + return scaled + + +def scale_interval_document( + document: Mapping, scalings: Mapping[str, Mapping[str, object]] +) -> dict: + """Return a document with only the named source tables scaled.""" + + tables = validate_interval_document(document) + if any(table.scaled for table in tables.values()): + raise ValueError("A base interval document cannot contain scaled tables.") + unknown = set(scalings) - set(tables) + if unknown: + raise ValueError(f"Unknown interval tables {sorted(unknown)}.") + effective = copy.deepcopy(dict(document)) + effective["tables"] = [ + scale_interval_table(raw, **scalings[raw["id"]]) + if raw["id"] in scalings + else raw + for raw in effective["tables"] + ] + validate_interval_document(effective) + return effective + + +def source_interval_table(table: Mapping) -> dict: + """The source declaration of a scaled table, or the table itself.""" + + parsed = interval_table(table) + return _source_table(parsed.declaration) if parsed.scaled else dict(table) + + +# --------------------------------------------------------------------------- +# Classification +# --------------------------------------------------------------------------- + + +def _amount_reason(value: object) -> str | None: + if isinstance(value, (bool, np.bool_)): + return "boolean_amount" + if isinstance(value, Integral): + return None + if value is None: + return "missing_amount" + if isinstance(value, float): + if math.isnan(value): + return "missing_amount" + return "non_integer_amount" if math.isfinite(value) else "nonfinite_amount" + if isinstance(value, str): + return "text_amount" + try: + if value != value: # pandas NA, numpy NaN scalars + return "missing_amount" + except TypeError: + return "missing_amount" + return "non_integer_amount" + + +def _validated( + table: IntervalTable, amounts: Sequence[object], universe: Sequence[object] +) -> list[int | None]: + """Validate every record, then return floored cents or None when excluded.""" + + amounts, universe = list(amounts), list(universe) + if len(amounts) != len(universe): + raise ValueError("Interval amounts and universe statuses must align.") + reasons, statuses, floored = Counter(), Counter(), [] + for amount, status in zip(amounts, universe, strict=True): + if not isinstance(status, str) or status not in UNIVERSE_STATES: + reasons["invalid_universe_status"] += 1 + statuses["invalid"] += 1 + floored.append(None) + continue + statuses[status] += 1 + if status == "excluded": + floored.append(None) + continue + if status == "unresolved": + reasons["unresolved_universe_status"] += 1 + floored.append(None) + continue + reason = _amount_reason(amount) + if reason is not None: + reasons[reason] += 1 + floored.append(None) + continue + value = int(amount) + if value < 0 and table.negative_income_policy == "refuse": + reasons["negative_amount_refused"] += 1 + floored.append(None) + continue + floored.append(max(0, value)) + if reasons: + raise IntervalPreparationError( + { + "table": table.id, + "entity": table.entity, + "records": len(amounts), + "refused_records": sum(reasons.values()), + "reasons": dict(sorted(reasons.items())), + "universe_statuses": dict(sorted(statuses.items())), + } + ) + return floored + + +def _cuts_at_or_below(z: int, cuts: Sequence[Fraction], inclusive: bool) -> int: + """Count cuts ``c`` with ``c < z`` (or ``c <= z``) by cross-multiplication.""" + + low, high = 0, len(cuts) + while low < high: + middle = (low + high) // 2 + cut = cuts[middle] + scaled = z * cut.denominator + if cut.numerator < scaled or (inclusive and cut.numerator == scaled): + low = middle + 1 + else: + high = middle + return low + + +def _position(table: IntervalTable, z: int, closure: str) -> int: + if table.nil_present and z == 0: + return table.nil_index + # Right-closed bands are ``lower < z <= upper`` and the tail ``z > last``; + # left-closed bands are ``lower <= z < upper`` and the tail ``z >= last``. + count = _cuts_at_or_below(z, table.cuts, inclusive=closure == "left") + return BELOW_FIRST_CUT if count == 0 else table.positive_indices[count - 1] + + +def classify_records( + table: IntervalTable, + amounts: Sequence[object], + universe: Sequence[object], + *, + closure: str | None = None, +) -> tuple[int, ...]: + """Band index per record, ``BELOW_FIRST_CUT`` or ``EXCLUDED``. + + Every record is validated first; any refusal raises before a band is + computed. ``closure`` defaults to the table's own declaration. + """ + + closure = ( + table.closure if closure is None else _choice(closure, CLOSURES, "closure") + ) + floored = _validated(table, amounts, universe) + return tuple( + EXCLUDED if z is None else _position(table, z, closure) for z in floored + ) + + +def interval_indicators( + table: IntervalTable, amounts: Sequence[object], universe: Sequence[object] +) -> dict[str, np.ndarray]: + """One Boolean indicator per band, produced only after validation passes.""" + + positions = np.asarray(classify_records(table, amounts, universe), dtype=np.int64) + return { + indicator: positions == index + for index, indicator in enumerate(table.indicators) + } + + +def _fingerprint_value(value: object) -> object: + if isinstance(value, (bool, np.bool_)): + return ["bool", bool(value)] + if isinstance(value, Integral): + return int(value) + if isinstance(value, str): + return value + return [type(value).__name__, repr(value)] + + +def interval_input_fingerprint(values: Sequence[object]) -> str: + """Digest of an input column that keeps value types, including invalid ones.""" + + return interval_document_sha256([_fingerprint_value(value) for value in values]) + + +# --------------------------------------------------------------------------- +# Boundary diagnostics +# --------------------------------------------------------------------------- + + +def _record_order(record_ids: Sequence[object]) -> list: + result = [] + for value in record_ids: + if isinstance(value, (bool, np.bool_)) or not isinstance( + value, (str, Integral) + ): + raise ValueError("Diagnostic record identities are strings or integers.") + result.append(value if isinstance(value, str) else int(value)) + if len(set(result)) != len(result): + raise ValueError("Diagnostic record identities must be unique.") + return result + + +def boundary_diagnostics( + table: IntervalTable, + record_ids: Sequence[object], + amounts: Sequence[object], + universe: Sequence[object], + weights: Sequence[float], + *, + weight_record_ids: Sequence[object], + weight_kind: str, +) -> dict[str, object]: + """Fixed-weight boundary sensitivity, computed with exact weight arithmetic.""" + + order = _record_order(record_ids) + if _record_order(weight_record_ids) != order: + raise ValueError("Diagnostic weights are not aligned with the record order.") + values = np.asarray(weights, dtype=" 0 else "none" + ] + else: + destination = destinations["positive_bands"][ + table.band_ids[position] + ] + destination[0] += exact + destination[1] += exact * raw_negative + destination[2] += 1 + destination[3] += raw_negative + first = _cuts_at_or_below(z - window, table.cuts, inclusive=False) + last = _cuts_at_or_below(z + window, table.cuts, inclusive=True) + for index in range(first, last): + near[index][0] += exact + near[index][1] += 1 + cut_names = ["first_lower_cut"] + [ + table.band_ids[index] for index in table.positive_indices[:-1] + ] + source = _source_table(table.declaration) if table.scaled else table.declaration + source_cuts = _source_cuts(source) + source_values = [source_cuts["first_lower_cut_cents"], *source_cuts["upper_cents"]] + cuts = [ + { + "cut": name, + "cut_cents": _rational(cut), + "source_cut_cents": source_values[index], + "near_cut_weighted_count": _rational(near[index][0]), + "near_cut_records": near[index][1], + } + for index, (name, cut) in enumerate(zip(cut_names, table.cuts, strict=True)) + ] + + def measures(sums): + count, amount, records = sums + return { + "weighted_count": _rational(count), + "weighted_amount_cents": _rational(amount), + "records": records, + } + + def delta(slot): + return { + "weighted_count": _rational(other[slot][0] - declared[slot][0]), + "weighted_amount_cents": _rational(other[slot][1] - declared[slot][1]), + "records": other[slot][2] - declared[slot][2], + } + + def floored_measures(sums): + count, weighted_amount, records, amount = sums + return { + "records": records, + "weighted_count": _rational(count), + "pre_floor_amount_cents": amount, + "weighted_pre_floor_amount_cents": _rational(weighted_amount), + } + + def floored_by_destination(closure): + destinations = floored_destinations[closure] + return { + "nil": floored_measures(destinations["nil"]), + "below_first_cut": floored_measures(destinations["below_first_cut"]), + "none": floored_measures(destinations["none"]), + "positive_bands": { + band_id: floored_measures(sums) + for band_id, sums in destinations["positive_bands"].items() + }, + } + + if table.nil_index is not None and ( + declared[table.nil_index] != other[table.nil_index] + ): + raise ValueError("The nil band must be identical under both closures.") + bands = [] + for index, band_id in enumerate(table.band_ids): + row = { + "band": band_id, + "kind": "nil" if index == table.nil_index else "positive", + "declared": measures(declared[index]), + } + if index != table.nil_index: + row["alternative"] = measures(other[index]) + row["delta"] = delta(index) + bands.append(row) + scaling = table.declaration.get("scaling") + return { + "schema_version": 1, + "kind": "interval_boundary_diagnostics", + "table": table.id, + "entity": table.entity, + "currency": table.declaration["currency"], + "input_unit": table.declaration["input_unit"], + "positive_band_closure": table.closure, + "alternative_positive_band_closure": alternative, + "nil_band": "present" if table.nil_present else "absent", + "negative_income_policy": table.negative_income_policy, + "boundary_window_cents": window, + "weight_kind": weight_kind, + "records": len(order), + "included_records": included, + "digests": { + "table_sha256": interval_document_sha256(table.declaration), + "source_cuts_sha256": interval_document_sha256(source_cuts), + "effective_cuts_sha256": interval_document_sha256(_cut_payload(table)), + "factor": None + if scaling is None + else { + key: scaling[key] + for key in ( + "factor_id", + "operational_factor", + "factor_declaration_sha256", + "factor_receipt_sha256", + ) + }, + "weights_sha256": hashlib.sha256(values.tobytes()).hexdigest(), + "record_order_sha256": interval_document_sha256(order), + }, + "cuts": cuts, + "bands": bands, + "below_first_cut": { + "declared": measures(declared[BELOW_FIRST_CUT]), + "alternative": measures(other[BELOW_FIRST_CUT]), + "delta": delta(BELOW_FIRST_CUT), + }, + "floored_negative_income": { + **floored_measures(floored_negative), + "by_destination": { + "declared": floored_by_destination(table.closure), + "alternative": floored_by_destination(alternative), + }, + }, + "publisher_reclassification": PUBLISHER_RECLASSIFICATION, + "limitation": LIMITATION, + } + + +def frame_boundary_diagnostics(table: IntervalTable, frame) -> dict[str, object]: + """Diagnostics on a frame, weighted through ``resolve_weights(entity)``.""" + + data = frame.table(table.entity) + for column in (table.income_column, table.universe_column): + if column not in data: + raise ValueError( + f"Interval diagnostics need {table.entity}.{column} on the frame." + ) + weights = frame.resolve_weights(table.entity) + ids = data[frame.schema.entity_id_column(table.entity)].tolist() + return boundary_diagnostics( + table, + ids, + data[table.income_column].tolist(), + data[table.universe_column].tolist(), + weights.values, + weight_record_ids=ids, + weight_kind=weights.kind.value, + ) diff --git a/packages/microcosm-build/src/microcosm/build/reference_profiles.py b/packages/microcosm-build/src/microcosm/build/reference_profiles.py index 10015e8afc..5354a795bc 100644 --- a/packages/microcosm-build/src/microcosm/build/reference_profiles.py +++ b/packages/microcosm-build/src/microcosm/build/reference_profiles.py @@ -67,6 +67,8 @@ def _selection_identity(receipt: Mapping) -> dict: identity["selected_factor_identities"] = receipt["selected_factor_identities"] identity["factor_companions_sha256"] = receipt["factor_companions_sha256"] identity["transformed_names"] = receipt["transformed_names"] + if "interval_tables" in receipt: + identity["interval_tables_sha256"] = receipt["interval_tables_sha256"] return identity @@ -233,6 +235,190 @@ def _factor_companions(chosen: Mapping, by_name: Mapping) -> dict: return companions +def _interval_table_ids(transform: Mapping) -> list[str]: + tables = _sequence(transform.get("interval_table_ids"), "interval table ids") + if any(not isinstance(table, str) or not table for table in tables) or len( + set(tables) + ) != len(tables): + raise ValueError("Interval table ids must be unique nonempty text.") + return tables + + +def _validate_interval_companions( + transform: Mapping, tables: Mapping, by_name: Mapping, declaration: Mapping +) -> None: + """Every scaled band names exactly one count/money companion pair.""" + money, counts = _validate_companions(transform, by_name) + band_money, band_counts = [], [] + for table_id in _interval_table_ids(transform): + table = tables[table_id] + if ( + table.declaration["source_period"] != declaration["source_period"] + or table.declaration["target_period"] != declaration["target_period"] + ): + raise ValueError( + "Factor and interval table source/target periods are incompatible." + ) + for band in table.declaration["bands"]: + if "count_reference" not in band: + raise ValueError( + "An interval table without companion references cannot be scaled." + ) + pair = (band["count_reference"], band["amount_reference"]) + for name in pair: + row = by_name.get(name) + if ( + row is None + or row.get("entity") != table.entity + or row.get("filter") != band["indicator"] + ): + raise ValueError( + "Interval companions must filter on their band indicator." + ) + if _companion_key(by_name[pair[0]]) != _companion_key(by_name[pair[1]]): + raise ValueError( + "A band's count and money references are not companions." + ) + band_counts.append(pair[0]) + band_money.append(pair[1]) + if ( + len(set(band_money)) != len(band_money) + or len(set(band_counts)) != len(band_counts) + or set(band_money) != set(money) + or set(band_counts) != set(counts) + ): + raise ValueError( + "Interval band companions must be exactly the money and count rosters." + ) + + +def _scaled_intervals( + chosen: Mapping, + intervals: Mapping | None, + by_name: Mapping, + selected_factors: Mapping, +) -> tuple[dict | None, dict]: + """Scale each named table once with its transformation's selected factor.""" + requests = {} + for dimension, choice in chosen.items(): + for transform in choice["declaration"].get("transformations", []): + if transform.get("operation") != SCALE_MONEY_AND_INTERVALS: + continue + for table_id in _interval_table_ids(transform): + if table_id in requests: + raise ValueError( + f"Interval table {table_id!r} is scaled by more than one " + "transformation." + ) + requests[table_id] = (dimension, choice["name"], transform) + if not requests: + return intervals, {} + if intervals is None: + raise ValueError("Interval scaling needs the declared interval document.") + from microcosm.build.reference_intervals import ( + scale_interval_document, + validate_interval_document, + ) + + tables = validate_interval_document(intervals) + if set(requests) - set(tables): + raise ValueError("A profile names an undeclared interval table.") + scalings, records, checked = {}, {}, set() + for table_id, (dimension, profile, transform) in sorted(requests.items()): + factor_id = transform["factor_id"] + entry = selected_factors[factor_id] + if id(transform) not in checked: + _validate_interval_companions( + transform, tables, by_name, entry["declaration"] + ) + checked.add(id(transform)) + scalings[table_id] = { + "factor": entry["operational_factor"], + "factor_id": factor_id, + "factor_declaration_sha256": reference_document_sha256( + entry["declaration"] + ), + "factor_receipt_sha256": entry["receipt"]["receipt_sha256"], + } + records[table_id] = { + "dimension": dimension, + "profile": profile, + "factor_id": factor_id, + "operational_factor": _copy(entry["operational_factor"]), + "source_table_sha256": reference_document_sha256( + tables[table_id].declaration + ), + "bands": len(tables[table_id].band_ids), + } + effective = scale_interval_document(intervals, scalings) + for raw in effective["tables"]: + if raw["id"] in records: + records[raw["id"]]["effective_table_sha256"] = reference_document_sha256( + raw + ) + return effective, records + + +def _validate_interval_resolution( + resolution: Mapping, factors: Mapping, interval_document: Mapping | None +) -> None: + """Bind recorded interval scaling to selected factors and the document.""" + records = resolution.get("interval_tables") + if records is None: + if "interval_tables_sha256" in resolution or resolution.get( + "effective_intervals_sha256" + ) != resolution.get("base_intervals_sha256"): + raise ValueError( + "Unscaled effective intervals must equal their base document." + ) + return + records = _mapping(records, "interval scaling records") + if ( + resolution.get("resolver_version") != FACTOR_REFERENCE_PROFILE_RESOLVER_VERSION + or not records + or reference_document_sha256(records) + != resolution.get("interval_tables_sha256") + or resolution.get("effective_intervals_sha256") + == resolution.get("base_intervals_sha256") + ): + raise ValueError("Interval scaling record identity mismatch.") + identities = resolution.get("selected_factor_identities", {}) + for record in records.values(): + record = _mapping(record, "interval scaling record") + factor_id = record.get("factor_id") + if ( + factor_id not in factors + or record.get("operational_factor") + != factors[factor_id]["operational_factor"] + or record.get("profile") + != resolution["selection"].get(record.get("dimension")) + ): + raise ValueError("Interval scaling disagrees with its selected factor.") + if interval_document is None: + return + from microcosm.build.reference_intervals import validate_interval_document + + tables = validate_interval_document(interval_document) + scaled = {table_id for table_id, table in tables.items() if table.scaled} + if scaled != set(records): + raise ValueError("Effective interval tables differ from their scaling records.") + for table_id in scaled: + record, declaration = records[table_id], tables[table_id].declaration + scaling = declaration["scaling"] + identity = identities.get(record["factor_id"], {}) + if ( + reference_document_sha256(declaration) + != record.get("effective_table_sha256") + or scaling["source_table_sha256"] != record.get("source_table_sha256") + or scaling["factor_id"] != record["factor_id"] + or scaling["operational_factor"] != record["operational_factor"] + or scaling["factor_receipt_sha256"] != identity.get("receipt_sha256") + or scaling["factor_declaration_sha256"] + != identity.get("declaration_sha256") + ): + raise ValueError("Effective interval table scaling identity mismatch.") + + def _satisfy_factor_prerequisite(row: dict, dimension: str, binding: str) -> list: metadata = row.setdefault("metadata", {}) if metadata.get("activation_status") != REQUIRES_PREREQUISITES: @@ -280,8 +466,9 @@ def resolve_reference_profiles( Only selected declarations enter selection identity. An unavailable profile refuses; an explicitly deferred dimension is reported without claiming its transformations ran. Money scaling consumes declared factor evidence; - compilation authenticates it against the feed. Interval scaling is reserved - for the subsequent interval implementation. + compilation authenticates it against the feed. Named interval tables are + scaled by the same selected factor as their money companions; without + named tables the interval document passes through unchanged. Resource headers are bound into the base identity; effective references use the compiler's closed document shape. """ @@ -336,8 +523,7 @@ def resolve_reference_profiles( for raw_transform in choice["declaration"].get("transformations", []): transform = _mapping(raw_transform, "reference transformation") if transform.get("operation") == SCALE_MONEY_AND_INTERVALS: - if _sequence(transform.get("interval_table_ids"), "interval table ids"): - raise ValueError("interval scaling lands in r2b") + _interval_table_ids(transform) factor_id = transform.get("factor_id") if not isinstance(factor_id, str) or not factor_id: raise ValueError("Money scaling needs a declared factor id.") @@ -347,12 +533,16 @@ def resolve_reference_profiles( from microcosm.build.reference_factors import validate_factor_context selected_factors = validate_factor_context(factor_context, sorted(factor_ids)) + base_intervals_hash = ( + None if intervals is None else reference_document_sha256(intervals) + ) + intervals, interval_records = _scaled_intervals( + chosen, intervals, by_name, selected_factors + ) receipt = { "resolver_version": REFERENCE_PROFILE_RESOLVER_VERSION, "base_references_sha256": reference_document_sha256(base), - "base_intervals_sha256": ( - None if intervals is None else reference_document_sha256(intervals) - ), + "base_intervals_sha256": base_intervals_hash, "selected_declarations_sha256": reference_document_sha256(chosen), "selection": resolved_selection, } @@ -384,6 +574,11 @@ def resolve_reference_profiles( for factor_id, entry in sorted(selected_factors.items()) }, ) + if interval_records: + receipt.update( + interval_tables=interval_records, + interval_tables_sha256=reference_document_sha256(interval_records), + ) selection_hash = reference_document_sha256(_selection_identity(receipt)) interval_hash = None if intervals is None else reference_document_sha256(intervals) transformed, policies = {}, [] @@ -420,11 +615,6 @@ def resolve_reference_profiles( if _carried_value_keys(transform): raise ValueError("Reference transformations carry observed values.") if operation == SCALE_MONEY_AND_INTERVALS: - tables = _sequence( - transform.get("interval_table_ids"), "interval table ids" - ) - if tables: - raise ValueError("interval scaling lands in r2b") binding = transform.get("required_measurement_binding") if not isinstance(binding, str) or not binding: raise ValueError( @@ -639,6 +829,7 @@ def validate_reference_resolution( interval_document ) != resolution.get("effective_intervals_sha256"): raise ValueError("Effective interval document hash mismatch.") + _validate_interval_resolution(resolution, factors, interval_document) factor_bindings = _factor_reference_bindings(resolution) transformed_names = resolution.get("transformed_names") if not isinstance(transformed_names, list) or not all( diff --git a/packages/microcosm-build/src/microcosm/build/transport/compose.py b/packages/microcosm-build/src/microcosm/build/transport/compose.py index b0907925d3..4c7a91417c 100644 --- a/packages/microcosm-build/src/microcosm/build/transport/compose.py +++ b/packages/microcosm-build/src/microcosm/build/transport/compose.py @@ -1240,9 +1240,17 @@ def _node( ): view = reference_profile_view(spec, profile_selection) if view is not None: - references, _, receipt = view + references, intervals, receipt = view if canonical_json(references).decode("utf-8") == params["references"]: params["reference_resolution"] = canonical_json(receipt).decode("utf-8") + # Preparation receives the exact effective interval companion. + if kernel == "calibration.prepare@1" and intervals is not None: + companion = canonical_json(intervals).decode("utf-8") + if params.setdefault("intervals", companion) != companion: + raise ValueError( + f"Node {row.get('id')!r} supplies an interval document " + "that differs from its effective interval companion." + ) if "rules_binding" in row: params = _rules_params(spec, row, params, config) elif kernel == RULES_ROUTER: diff --git a/packages/microcosm-build/src/microcosm/build/transport/target_kernels.py b/packages/microcosm-build/src/microcosm/build/transport/target_kernels.py index efd5fbe291..46f25de2fa 100644 --- a/packages/microcosm-build/src/microcosm/build/transport/target_kernels.py +++ b/packages/microcosm-build/src/microcosm/build/transport/target_kernels.py @@ -54,6 +54,7 @@ import microcosm.build.ledger_artifact as ledger_artifact_module import microcosm.build.ledger_targets as ledger_targets_module import microcosm.build.reference_factors as reference_factors_module +import microcosm.build.reference_intervals as reference_intervals_module import microcosm.build.reference_profiles as reference_profiles_module import microcosm.calibrate.artifacts as calibrate_artifacts_module import microcosm.calibrate.hierarchy as hierarchy_module @@ -557,6 +558,8 @@ def implementation_hash(self) -> str: graph_inputs, artifact_types, calibration_columns_module, + reference_intervals_module, + reference_factors_module, reference_profiles_module, dependencies=self.capabilities.dependencies, ) @@ -585,6 +588,12 @@ def run(self, context: KernelContext) -> KernelResult: bindings = canonical_document_param(context, self.ref, "bindings") document = canonical_document_param(context, self.ref, "references") digest = sha256_param(context, self.ref, "references_sha256") + # Parsed once: the receipt check and column preparation read one document. + intervals = ( + canonical_document_param(context, self.ref, "intervals") + if "intervals" in context.params + else None + ) resolution = None if "reference_resolution" in context.params: candidate = canonical_document_param( @@ -592,7 +601,7 @@ def run(self, context: KernelContext) -> KernelResult: ) if ( candidate.get("effective_intervals_sha256") is not None - and "intervals" not in context.params + and intervals is None ): raise ValueError( "Calibration preparation requires its effective interval companion." @@ -601,11 +610,7 @@ def run(self, context: KernelContext) -> KernelResult: document, candidate, references_sha256=digest, - interval_document=( - canonical_document_param(context, self.ref, "intervals") - if "intervals" in context.params - else None - ), + interval_document=intervals, ) elif any( row.get("metadata", {}).get("reference_profiles_resolved") @@ -614,7 +619,13 @@ def run(self, context: KernelContext) -> KernelResult: raise ValueError( "Profile-resolved preparation requires a reference resolution receipt." ) - tables = prepare_calibration_columns(context.tables, bindings) + if intervals is not None and resolution is None: + raise ValueError( + "An interval companion requires its reference resolution receipt." + ) + tables = prepare_calibration_columns( + context.tables, bindings, intervals=intervals + ) inventory = { (row["entity"], row["name"]) for row in (*bindings["columns"], *bindings.get("roster_counts", ())) @@ -652,6 +663,10 @@ def run(self, context: KernelContext) -> KernelResult: for entity, name in sorted(inventory) ], } + interval_inputs = _interval_input_evidence(context, bindings, intervals) + if interval_inputs: + evidence["effective_intervals"] = intervals + evidence["interval_inputs"] = interval_inputs payload = canonical_json(evidence) return KernelResult( columns=columns, @@ -660,13 +675,77 @@ def run(self, context: KernelContext) -> KernelResult: ) +def _interval_tables(bindings: Mapping, intervals: Mapping | None) -> list: + """The validated tables named by a binding's interval outputs, in order.""" + named = [] + for row in bindings.get("columns", ()): + if isinstance(row, Mapping) and "interval" in row: + if not isinstance(row["interval"], Mapping): + raise ValueError("An interval output names its table and band.") + table = row["interval"].get("table") + if table not in named: + named.append(table) + if not named: + return [] + if intervals is None: + raise ValueError( + "Interval outputs require their effective interval evidence; it is missing." + ) + tables = reference_intervals_module.validate_interval_document(intervals) + if set(named) - set(tables): + raise ValueError("Interval outputs name an undeclared interval table.") + return [tables[name] for name in named] + + +def _interval_input_rows(context: KernelContext, tables: Sequence) -> list[dict]: + rows = [] + for table in tables: + data = context.tables.get(table.entity) + if data is None or any( + column not in data + for column in ( + f"{table.entity}_id", + table.income_column, + table.universe_column, + ) + ): + raise ValueError( + f"Interval evidence for {table.id!r} is missing its " + f"{table.entity} income, universe or identity column." + ) + fingerprint = reference_intervals_module.interval_input_fingerprint + rows.append( + { + "table": table.id, + "entity": table.entity, + "income_column": table.income_column, + "universe_column": table.universe_column, + "rows": len(data), + "entity_ids_sha256": sha256_text( + canonical_json(data[f"{table.entity}_id"].tolist()) + ), + "income_values_sha256": fingerprint(data[table.income_column].tolist()), + "universe_values_sha256": fingerprint( + data[table.universe_column].tolist() + ), + } + ) + return rows + + +def _interval_input_evidence( + context: KernelContext, bindings: Mapping, intervals: Mapping | None +) -> list[dict]: + return _interval_input_rows(context, _interval_tables(bindings, intervals)) + + def _preparation_binding( context: KernelContext, surface: TargetSurface -) -> dict[str, object]: +) -> tuple[dict[str, object], list]: resolution = surface.reference_resolution artifact = context.artifacts.get("preparation") if resolution is None and artifact is None: - return {} + return {}, [] if artifact is None or artifact.type != CALIBRATION_PREPARATION_TYPE: raise ValueError( "Resolved targets require a typed calibration preparation artifact." @@ -720,11 +799,51 @@ def _preparation_binding( raise ValueError( "Calibration preparation inventory differs from the problem population." ) + intervals = evidence.get("effective_intervals") + if intervals is not None and sha256_text(canonical_json(intervals)) != evidence.get( + "effective_intervals_sha256" + ): + raise ValueError("Calibration preparation interval evidence hash mismatch.") + tables = _interval_tables(bindings, intervals) + if (intervals is None) != (not tables) or evidence.get( + "interval_inputs", [] + ) != _interval_input_rows(context, tables): + raise ValueError( + "Calibration preparation interval inputs differ from the problem population." + ) return { "reference_resolution": resolution, "preparation_sha256": sha256_text(artifact.payload), "preparation_artifact_key": artifact.key, **expected, + }, tables + + +def _interval_diagnostics( + frame: Frame, tables: Sequence, entities: Sequence[str], intervals_sha256 +) -> dict[str, object]: + """Fixed-weight boundary diagnostics; no reference value or target changes.""" + if not tables: + return {} + reports = [] + for table in tables: + if table.entity not in entities: + raise ValueError( + f"Interval diagnostics for {table.id!r} are missing evidence: the " + f"problem frame has no {table.entity!r} entity." + ) + reports.append( + reference_intervals_module.frame_boundary_diagnostics(table, frame) + ) + report = { + "schema_version": 1, + "kind": "interval_boundary_diagnostics_report", + "effective_intervals_sha256": intervals_sha256, + "tables": reports, + } + return { + "interval_diagnostics": report, + "interval_diagnostics_sha256": sha256_text(canonical_json(report)), } @@ -795,6 +914,7 @@ def implementation_hash(self) -> str: registry_module, hierarchy_module, calibrate_artifacts_module, + reference_intervals_module, *_MATRIX_MODULES, dependencies=self.capabilities.dependencies, ) @@ -811,10 +931,16 @@ def run(self, context: KernelContext) -> KernelResult: "under the alias 'surface'." ) surface = decode_target_surface(value.payload) - preparation = _preparation_binding(context, surface) + preparation, interval_tables = _preparation_binding(context, surface) frame = context_frame( context, self.ref, entities=entities, weight_entity=weight_entity ) + diagnostics = _interval_diagnostics( + frame, + interval_tables, + entities, + preparation.get("effective_intervals_sha256"), + ) problem = _compiled_problem(frame, surface.registry, weight_entity, self.ref) target_metadata = [ { @@ -841,6 +967,7 @@ def run(self, context: KernelContext) -> KernelResult: "entities": list(entities), "weight_entity": weight_entity, **preparation, + **diagnostics, }, ) return KernelResult( @@ -853,6 +980,7 @@ def run(self, context: KernelContext) -> KernelResult: "n_records": int(problem.n_weights), "weight_kind": problem.initial_weights.kind.value, **preparation, + **diagnostics, }, ) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_calibration_columns.py b/packages/microcosm-build/tests/engine_free/shared/test_calibration_columns.py index 283c146181..2c7c73cd21 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_calibration_columns.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_calibration_columns.py @@ -18,6 +18,10 @@ from microcosm.calibrate.matrix import build_constraint_matrix from microcosm.frame import EntitySchema, Frame, WeightKind, Weights from microcosm.frame.concepts import TENURE_DOMAIN +from test_support.microcosm_build.reference_intervals import ( + interval_bindings, + interval_table_fixture, +) from test_support.paths import paths_for @@ -187,6 +191,7 @@ def test_all_active_calibration_bindings_enter_the_small_frame_matrix(): json.loads((root / "target_references.json").read_text()), profiles, {}, + json.loads((root / profiles["interval_resource"]).read_text()), factor_context=json.loads( (root / profiles["factor_context_resource"]).read_text() ), @@ -340,3 +345,192 @@ def test_malformed_preparation_contracts_refuse(defect): bindings["columns"][0]["constant"] = float("nan") with pytest.raises(ValueError): prepare_calibration_columns(_tables(), bindings) + + +# --------------------------------------------------------------------------- +# Interval outputs: their own binding kind beside the predicate outputs +# --------------------------------------------------------------------------- + + +def _interval_contract(entity="person"): + table = interval_table_fixture(entity=entity, source_period=2002) + bindings = interval_bindings(table) + bindings["columns"].append({"entity": entity, "name": "counted", "constant": 1}) + return {"schema_version": 1, "tables": [table]}, bindings + + +def _interval_tables(amounts, statuses, entity="person"): + return { + entity: pd.DataFrame( + { + f"{entity}_id": range(len(amounts)), + "toy_income_cents": amounts, + "toy_income_universe": statuses, + } + ) + } + + +def test_interval_outputs_follow_the_universe_and_recompute_stale_values(): + intervals, bindings = _interval_contract() + tables = _interval_tables( + [-50, 0, 0, 100_000, 100_001, 5_000_000], + ["included", "included", "excluded", "included", "included", "excluded"], + ) + tables["person"]["toy_income_band_nil"] = [True] * 6 + prepared = prepare_calibration_columns(tables, bindings, intervals=intervals) + person = prepared["person"] + # A nonfiler with no income is excluded: never an all-person nil indicator. + assert person["toy_income_band_nil"].tolist() == [ + True, + True, + False, + False, + False, + False, + ] + assert person["toy_income_band_b0"].tolist() == [ + False, + False, + False, + True, + False, + False, + ] + assert person["toy_income_band_b1"].tolist() == [False] * 4 + [True, False] + assert person["toy_income_band_b2"].tolist() == [False] * 6 + assert person["toy_income_band_b0"].dtype == bool + assert person["counted"].tolist() == [1] * 6 + assert tables["person"]["toy_income_band_nil"].tolist() == [True] * 6 + tables["person"].loc[3, "toy_income_cents"] = 100_001 + fresh = prepare_calibration_columns(tables, bindings, intervals=intervals) + assert fresh["person"].loc[3, "toy_income_band_b1"] + assert not fresh["person"].loc[3, "toy_income_band_b0"] + + +@pytest.mark.parametrize( + ("amount", "status"), + [ + (10, "unresolved"), + (10, "unknown"), + (True, "included"), + (10.0, "included"), + ("10", "included"), + (pd.NA, "included"), + ], +) +def test_unresolved_universe_or_invalid_amount_refuses_before_any_output( + amount, status +): + intervals, bindings = _interval_contract() + tables = _interval_tables(pd.array([5, amount], dtype=object), ["included", status]) + before = copy.deepcopy(tables) + with pytest.raises(ValueError, match="refused before emitting indicators"): + prepare_calibration_columns(tables, bindings, intervals=intervals) + pd.testing.assert_frame_equal(tables["person"], before["person"]) + + +def test_household_entity_interval_outputs_are_entity_generic(): + intervals, bindings = _interval_contract(entity="household") + prepared = prepare_calibration_columns( + _interval_tables([0, 100_000], ["excluded", "included"], entity="household"), + bindings, + intervals=intervals, + ) + assert prepared["household"]["toy_income_band_b0"].tolist() == [False, True] + assert prepared["household"]["toy_income_band_nil"].tolist() == [False, False] + + +@pytest.mark.parametrize( + "defect", + [ + "no_document", + "unknown_table", + "unknown_band", + "wrong_entity", + "not_the_band_indicator", + "mixed_with_predicates", + "mixed_with_constant", + "extra_interval_field", + "unaged_source_cuts", + "missing_universe_column", + "missing_income_column", + "invalid_document", + ], +) +def test_malformed_interval_outputs_refuse(defect): + intervals, bindings = _interval_contract() + entry = bindings["columns"][1] + tables = _interval_tables([1, 2], ["included", "included"]) + if defect == "no_document": + intervals = None + elif defect == "unknown_table": + entry["interval"]["table"] = "other" + elif defect == "unknown_band": + entry["interval"]["band"] = "b9" + elif defect == "wrong_entity": + bindings["required_inputs"]["household"] = ["household_id"] + bindings["input_domains"]["household"] = {"household_id": {"kind": "identity"}} + tables["household"] = pd.DataFrame({"household_id": [0]}) + entry["entity"] = "household" + elif defect == "not_the_band_indicator": + entry["name"] = "renamed_indicator" + elif defect == "mixed_with_predicates": + entry["predicates"] = [{"column": "person_id", "operator": "ge", "value": 0}] + elif defect == "mixed_with_constant": + entry["constant"] = 1 + elif defect == "extra_interval_field": + entry["interval"]["closure"] = "left" + elif defect == "unaged_source_cuts": + intervals["tables"][0]["source_period"] = 2001 + elif defect == "missing_universe_column": + tables["person"] = tables["person"].drop(columns="toy_income_universe") + elif defect == "missing_income_column": + tables["person"] = tables["person"].drop(columns="toy_income_cents") + else: + intervals["tables"][0].pop("positive_band_closure") + with pytest.raises(ValueError): + prepare_calibration_columns(tables, bindings, intervals=intervals) + + +def test_interval_outputs_enter_a_constraint_row_through_their_filter(): + intervals, bindings = _interval_contract() + tables = _interval_tables([0, 50_000, 150_000], ["included"] * 3) + tables["household"] = pd.DataFrame({"household_id": [0, 1, 2]}) + tables["person"]["person_household_id"] = [0, 1, 2] + prepared = prepare_calibration_columns(tables, bindings, intervals=intervals) + frame = _frame(prepared) + target = Target( + name="first band", + entity="person", + measure="toy_income_cents", + filter="toy_income_band_b0", + ) + assert target.constraint_row(frame).tolist() == [0, 50_000, 0] + + +def test_signed_integer_domain_admits_losses_and_refuses_booleans_and_floats(): + bindings = { + "schema_version": 1, + "required_inputs": {"person": ["person_id", "net_cents"]}, + "input_domains": { + "person": { + "person_id": {"kind": "identity"}, + "net_cents": {"kind": "signed_integer"}, + } + }, + "columns": [ + { + "entity": "person", + "name": "loss_indicator", + "predicates": [{"column": "net_cents", "operator": "lt", "value": 0}], + } + ], + } + tables = {"person": pd.DataFrame({"person_id": [0, 1], "net_cents": [-5, 7]})} + prepared = prepare_calibration_columns(tables, bindings) + assert prepared["person"]["loss_indicator"].tolist() == [True, False] + for values in ([True, False], [-5.0, 7.0], pd.array([-5, None], dtype="Int64")): + tables["person"]["net_cents"] = values + with pytest.raises(ValueError): + prepare_calibration_columns(tables, bindings) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_nz_reference_intervals.py b/packages/microcosm-build/tests/engine_free/shared/test_nz_reference_intervals.py new file mode 100644 index 0000000000..02908e1510 --- /dev/null +++ b/packages/microcosm-build/tests/engine_free/shared/test_nz_reference_intervals.py @@ -0,0 +1,340 @@ +"""NZ IRD interval declarations: generated cuts, companions and the declared convention. + +The row formulas below are NZ data, checked here against the packaged +resource; the shared interval module contains none of them. +""" + +from __future__ import annotations + +import json +import os +from fractions import Fraction +from pathlib import Path + +import pytest + +from microcosm.build.ledger_artifact import load_ledger_consumer_artifact +from microcosm.build.reference_intervals import ( + classify_records, + interval_table, + validate_interval_document, +) +from microcosm.build.reference_profiles import resolve_reference_profiles +from test_support.paths import paths_for + +_SPEC = paths_for("microcosm-build").package / "src/microcosm/build/nz" +_REPOSITORY = paths_for("microcosm-build").repository +NATIONAL = "ird_taxable_income_national_ty2024" +AGE = "ird_taxable_income_age_ty2024" +IRD_REASON = ( + "B4: reviewed IRD-definition taxable-income measurement, annual component " + "closure and record universe remain required. Ageing resolution does not " + "establish that measurement." +) + + +def _document(name): + return json.loads((_SPEC / name).read_text()) + + +def _national_upper(row): + if row == 9: + return 100 + if row == 10: + return 1_000 + if row <= 209: + return 1_000 * (row - 9) + if row <= 229: + return 200_000 + 5_000 * (row - 209) + if row <= 243: + return 300_000 + 50_000 * (row - 229) + return None + + +def _age_upper(row): + if row == 7: + return 100 + if row == 8: + return 1_000 + if row <= 187: + return 1_000 * (row - 7) + return None + + +def _tables(): + return { + table["id"]: table for table in _document("reference_intervals.json")["tables"] + } + + +def _upper_dollars(band): + return None if band["upper_cent"] is None else band["upper_cent"] / 100 + + +def test_document_validates_under_the_shared_contract_with_declared_semantics(): + parsed = validate_interval_document(_document("reference_intervals.json")) + assert list(parsed) == [NATIONAL, AGE] + for table in _tables().values(): + assert { + key: table[key] + for key in ( + "entity", + "currency", + "input_unit", + "income_column", + "universe_column", + "source_period", + "target_period", + "positive_band_closure", + "nil_band", + "negative_income_policy", + "first_lower_cut_cents", + "diagnostics", + ) + } == { + "entity": "person", + "currency": "NZD", + "input_unit": "integer_cent", + "income_column": "ird_taxable_income_proxy_cents", + "universe_column": "ird_taxable_income_proxy_universe_status", + "source_period": 2024, + "target_period": 2027, + "positive_band_closure": "right", + "nil_band": "present", + "negative_income_policy": "floor_zero", + "first_lower_cut_cents": 0, + "diagnostics": { + "boundary_window_cents": 100, + "alternative_positive_band_closure": "left", + }, + } + assert "scaling" not in table + + +def test_national_table_has_237_count_money_interval_triples_and_no_total_row(): + table = _tables()[NATIONAL] + assert table["source_locator"] == "Income tables 2001-24!BE8:BE244" + bands = table["bands"] + assert [band["id"] for band in bands] == [f"row_{row}" for row in range(8, 245)] + assert not any("246" in json.dumps(band) for band in bands) + assert bands[0] == { + "id": "row_8", + "kind": "nil", + "indicator": "taxable_income_band_row_8_indicator", + "source_label": "nil", + "count_reference": "ird_taxable_income_people_by_band__row_8", + "amount_reference": "ird_taxable_income_total_by_band__row_8", + } + for band in bands[1:]: + row = int(band["id"].removeprefix("row_")) + assert band["kind"] == "positive" + assert _upper_dollars(band) == _national_upper(row) + assert bands[-1]["source_label"] == "Over $1,000,000" + references = { + row["name"]: row + for row in _document("target_references.json")["target_references"] + } + profiles = _document("reference_profiles.json") + for profile in ("low", "central", "high"): + (transformation,) = profiles["dimensions"]["aging_index"]["profiles"][profile][ + "transformations" + ] + assert transformation["interval_table_ids"] == [NATIONAL] + assert transformation["count_reference_names"] == [ + band["count_reference"] for band in bands + ] + assert transformation["reference_names"] == [ + band["amount_reference"] for band in bands + ] + for band in bands: + for name in (band["count_reference"], band["amount_reference"]): + assert references[name]["filter"] == band["indicator"] + assert references[name]["entity"] == "person" + + +def test_age_table_has_183_bands_and_no_companions_or_fit_rows(): + table = _tables()[AGE] + assert table["source_locator"] == "A6:A188" + bands = table["bands"] + assert [band["id"] for band in bands] == [f"row_{row}" for row in range(6, 189)] + assert bands[0]["kind"] == "nil" + for band in bands[1:]: + assert _upper_dollars(band) == _age_upper(int(band["id"].removeprefix("row_"))) + assert bands[-1]["source_label"] == "Over $180,000" + assert all( + "count_reference" not in band and "amount_reference" not in band + for band in bands + ) + indicators = {band["indicator"] for band in bands} + for name in ("target_references.json", "precal_references.json"): + assert not any( + row.get("filter") in indicators + for row in _document(name)["target_references"] + ) + profiles = json.dumps(_document("reference_profiles.json")) + assert AGE not in profiles + + +@pytest.mark.parametrize( + ("table", "rows", "endpoint"), + [ + (NATIONAL, (29, 30), 20_000), + (NATIONAL, (59, 60), 50_000), + (NATIONAL, (89, 90), 80_000), + (NATIONAL, (229, 230), 300_000), + (AGE, (27, 28), 20_000), + (AGE, (57, 58), 50_000), + (AGE, (87, 88), 80_000), + ], +) +def test_shared_endpoint_labels_resolve_to_the_ordered_cuts(table, rows, endpoint): + declaration = _tables()[table] + by_id = {band["id"]: band for band in declaration["bands"]} + lower, upper = (by_id[f"row_{row}"] for row in rows) + label = f"${endpoint:,}" + # Both published labels name the endpoint; the ordered cut gives it to the + # lower band under the declared right-closed convention. + assert lower["source_label"].replace(" ", "").endswith(label) + assert upper["source_label"].replace(" ", "").startswith(label) + assert lower["upper_cent"] == endpoint * 100 + parsed = interval_table(declaration) + positions = classify_records( + parsed, + [endpoint * 100 - 1, endpoint * 100, endpoint * 100 + 1], + ["included"] * 3, + ) + assert [parsed.band_ids[index] for index in positions] == [ + lower["id"], + lower["id"], + upper["id"], + ] + + +def test_central_ageing_keeps_fractional_cent_cuts_exactly(): + _, intervals, receipt = resolve_reference_profiles( + _document("target_references.json"), + _document("reference_profiles.json"), + {}, + _document("reference_intervals.json"), + factor_context=_document("reference_aging.json"), + ) + tables = {table["id"]: table for table in intervals["tables"]} + national = tables[NATIONAL] + by_id = {band["id"]: band for band in national["bands"]} + cuts = { + row: Fraction( + by_id[f"row_{row}"]["upper_cent"]["numerator"], + by_id[f"row_{row}"]["upper_cent"]["denominator"], + ) + for row in (9, 10, 49) + } + # $100, $1,000 and $40,000 at the central factor 1.117109. + assert cuts == { + 9: Fraction("11171.09"), + 10: Fraction("111710.9"), + 49: Fraction(4_468_436), + } + parsed = interval_table(national) + positions = classify_records( + parsed, [11_171, 11_172, 111_710, 111_711], ["included"] * 4 + ) + assert [parsed.band_ids[index] for index in positions] == [ + "row_9", + "row_10", + "row_10", + "row_11", + ] + assert tables[AGE] == _tables()[AGE] + assert receipt["interval_tables"][NATIONAL]["factor_id"] == ( + "ird_ty2024_to_ty2027_central" + ) + + +def test_resolution_is_not_activation_and_no_active_row_requests_the_proxy(): + intervals = _document("reference_intervals.json") + proxy = { + "ird_taxable_income_proxy_cents", + "ird_taxable_income_proxy_universe_status", + } + indicators = { + band["indicator"] for table in intervals["tables"] for band in table["bands"] + } + for profile in ("low", "central", "high"): + effective, _, _ = resolve_reference_profiles( + _document("target_references.json"), + _document("reference_profiles.json"), + {"aging_index": profile}, + intervals, + factor_context=_document("reference_aging.json"), + ) + ird = [ + row + for row in effective["target_references"] + if row["name"].startswith("ird_taxable_income_") + ] + assert len(ird) == 474 + assert all( + row["metadata"]["activation_status"] == "requires_prerequisites" + and row["metadata"]["parking_reason"] == IRD_REASON + for row in ird + ) + for row in effective["target_references"]: + if row["metadata"].get("activation_status", "active") == "active": + assert row.get("filter") not in indicators + assert row.get("measure") not in proxy + bindings = json.dumps(_document("calibration_bindings.json")) + assert not any(name in bindings for name in proxy | indicators) + precal = _document("precal_references.json")["target_references"] + wages = [row for row in precal if row["name"].startswith("ird_wage_salary_")] + assert len(wages) == 235 + assert all( + row["metadata"]["activation_status"] + == "requires_g8r2_source_period_wage_bridge_and_intervals" + for row in wages + ) + + +def test_declared_convention_is_disclosed_in_the_resource_and_the_docs(): + disclosure = _document("reference_intervals.json")["disclosure"] + for phrase in ( + "IRD publishes no band closure or cents semantics", + "Microcosm's declared", + "convention", + "publisher reclassification is unavailable", + ): + assert phrase in disclosure + docs = (_REPOSITORY / "docs/nz-calibration-targets.md").read_text() + assert "IRD publishes no band closure or cents semantics" in docs + assert "declared convention" in docs + + +@pytest.fixture(scope="module") +def artifact(): + path = os.environ.get("NZ_CHRONICLE_ARTIFACT") + if not path: + pytest.skip("NZ_CHRONICLE_ARTIFACT is unset; public NZ feed audit unavailable") + return load_ledger_consumer_artifact( + Path(path), feed_pin=_document("chronicle_feed.json") + ) + + +def test_every_band_label_is_the_feed_label_for_its_row(artifact): + record_sets = { + NATIONAL: "ird_taxable_income_distribution.ty2024.income_bands", + AGE: "ird_taxable_income_distribution.ty2024.age_by_income_band", + } + for table_id, record_set in record_sets.items(): + labels = {} + for fact in artifact.facts: + layout = fact.get("layout", {}) + if ( + layout.get("record_set_id") == record_set + and layout.get("table_record_kind") == "detail" + ): + labels.setdefault(layout["source_row_id"], set()).add( + fact["dimensions"]["taxable_income_band_source_label"] + ) + bands = _tables()[table_id]["bands"] + assert set(labels) == {band["id"] for band in bands} + for band in bands: + assert labels[band["id"]] == {band["source_label"]} diff --git a/packages/microcosm-build/tests/engine_free/shared/test_nz_target_references.py b/packages/microcosm-build/tests/engine_free/shared/test_nz_target_references.py index 3c6ebed1c1..0a9d1f53a9 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_nz_target_references.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_nz_target_references.py @@ -111,6 +111,7 @@ def _effective_rows(profile="mar"): _document("target_references.json"), _document("reference_profiles.json"), {"census_residual_allocation": profile, "aging_index": "central"}, + _document("reference_intervals.json"), factor_context=_document("reference_aging.json"), ) return document["target_references"] @@ -267,9 +268,13 @@ def test_full_nz_resource_resolves_directly_to_the_strict_compiler_document(prof raw, _document("reference_profiles.json"), {"census_residual_allocation": profile, "aging_index": "central"}, + _document("reference_intervals.json"), factor_context=_document("reference_aging.json"), ) - assert intervals is None + assert receipt["base_intervals_sha256"] == reference_document_sha256( + _document("reference_intervals.json") + ) + assert ["scaling" in table for table in intervals["tables"]] == [True, False] assert set(effective) == set(raw) & REFERENCE_DOCUMENT_KEYS references = parse_reference_document(effective, country="nz") assert len(references) == 773 @@ -359,16 +364,22 @@ def test_ageing_profiles_have_237_explicit_companions_and_keep_b4_parked(profile ] assert transformation["count_reference_names"] == expected_counts assert transformation["reference_names"] == expected_money - assert transformation["interval_table_ids"] == [] + assert transformation["interval_table_ids"] == [ + "ird_taxable_income_national_ty2024" + ] assert transformation["factor_id"] == f"ird_ty2024_to_ty2027_{profile}" assert not any("246" in name for name in expected_counts + expected_money) base = _document("target_references.json") - effective, _, receipt = resolve_reference_profiles( + effective, intervals, receipt = resolve_reference_profiles( base, profiles, {"census_residual_allocation": "mar", "aging_index": profile}, + _document("reference_intervals.json"), factor_context=_document("reference_aging.json"), ) + assert intervals["tables"][0]["scaling"]["factor_id"] == ( + f"ird_ty2024_to_ty2027_{profile}" + ) before = {row["name"]: row for row in base["target_references"]} after = {row["name"]: row for row in effective["target_references"]} factor = {"low": 1.115056, "central": 1.117109, "high": 1.119158}[profile] diff --git a/packages/microcosm-build/tests/engine_free/shared/test_reference_intervals.py b/packages/microcosm-build/tests/engine_free/shared/test_reference_intervals.py new file mode 100644 index 0000000000..e9743590be --- /dev/null +++ b/packages/microcosm-build/tests/engine_free/shared/test_reference_intervals.py @@ -0,0 +1,1129 @@ +"""Exact interval cuts, declared semantics, indicators and boundary diagnostics. + +The Hypothesis oracle below reads cuts itself with ``fractions.Fraction`` and +never calls a production helper. +""" + +from __future__ import annotations + +import copy +import json +from decimal import Decimal +from fractions import Fraction + +import numpy as np +import pandas as pd +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from microcosm.build.reference_intervals import ( + BELOW_FIRST_CUT, + EXCLUDED, + IntervalPreparationError, + boundary_diagnostics, + classify_records, + frame_boundary_diagnostics, + interval_document_sha256, + interval_indicators, + interval_input_fingerprint, + interval_table, + scale_interval_document, + scale_interval_table, + source_interval_table, + validate_interval_document, +) +from microcosm.frame import EntitySchema, Frame, WeightKind, Weights + +CENTRAL = {"numerator": 1117109, "denominator": 1000000} +_SETTINGS = settings( + max_examples=60, + database=None, + deadline=None, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) + + +def _table( + cuts=(10_000, 100_000), + *, + closure="right", + nil=True, + first=0, + entity="person", + policy="floor_zero", + table_id="table", + window=100, + income="income_cents", + universe="universe_status", +): + bands = [] + if nil: + bands.append({"id": "nil", "kind": "nil", "indicator": f"{table_id}_nil"}) + for index, cut in enumerate((*cuts, None)): + bands.append( + { + "id": f"b{index}", + "kind": "positive", + "upper_cent": cut, + "indicator": f"{table_id}_b{index}", + } + ) + return { + "id": table_id, + "entity": entity, + "currency": "XTS", + "input_unit": "integer_cent", + "income_column": income, + "universe_column": universe, + "source_period": 2001, + "target_period": 2001, + "positive_band_closure": closure, + "nil_band": "present" if nil else "absent", + "negative_income_policy": policy, + "first_lower_cut_cents": first, + "source_locator": "Toy!A1:A9", + "diagnostics": { + "boundary_window_cents": window, + "alternative_positive_band_closure": "left" + if closure == "right" + else "right", + }, + "bands": bands, + } + + +def _scale(table, factor=CENTRAL): + return scale_interval_table( + table, + factor, + factor_id="toy_factor", + factor_declaration_sha256="a" * 64, + factor_receipt_sha256="b" * 64, + ) + + +def _ids(table, positions): + parsed = interval_table(table) + return [ + {BELOW_FIRST_CUT: "below", EXCLUDED: "excluded"}.get( + position, parsed.band_ids[position] if position >= 0 else None + ) + for position in positions + ] + + +def _classify(table, amounts, universe=None, **kwargs): + universe = ["included"] * len(amounts) if universe is None else universe + return _ids( + table, classify_records(interval_table(table), amounts, universe, **kwargs) + ) + + +# --------------------------------------------------------------------------- +# Example tests +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + ("closure", "expected"), + [ + ( + "right", + ["nil", "nil", "b0", "b0", "b0", "b1", "b1", "b1", "b2"], + ), + ( + "left", + ["nil", "nil", "b0", "b0", "b1", "b1", "b1", "b2", "b2"], + ), + ], +) +def test_each_exact_cut_and_one_cent_either_side(closure, expected): + table = _table(closure=closure, first=1 if closure == "left" else 0) + amounts = [-1, 0, 1, 9_999, 10_000, 10_001, 99_999, 100_000, 100_001] + assert _classify(table, amounts) == expected + + +@pytest.mark.parametrize( + ("closure", "zero", "last", "above"), + [("right", "below", "b1", "b2"), ("left", "b0", "b2", "b2")], +) +def test_first_positive_band_and_open_tail_under_both_closures( + closure, zero, last, above +): + table = _table(closure=closure, nil=False) + assert _classify(table, [0, 1, 100_000, 100_001]) == [zero, "b0", last, above] + + +@pytest.mark.parametrize("closure", ["right", "left"]) +@pytest.mark.parametrize("nil", [False, True]) +def test_a_table_starting_above_zero_reports_records_below_its_first_cut(closure, nil): + table = _table((5_000_000,), closure=closure, nil=nil, first=4_000_000) + amounts = [0, 3_999_999, 4_000_000, 4_000_001, 5_000_000, 5_000_001] + at_first = "below" if closure == "right" else "b0" + at_upper = "b0" if closure == "right" else "b1" + assert _classify(table, amounts) == [ + "nil" if nil else "below", + "below", + at_first, + "b0", + at_upper, + "b1", + ] + report = boundary_diagnostics( + interval_table(table), + list(range(6)), + amounts, + ["included"] * 6, + [1.0] * 6, + weight_record_ids=list(range(6)), + weight_kind="design", + ) + assert report["below_first_cut"]["declared"]["records"] == ( + 2 if closure == "right" else 1 + ) + (0 if nil else 1) + assert Fraction(**report["below_first_cut"]["declared"]["weighted_count"]) == ( + 2 if closure == "right" else 1 + ) + (0 if nil else 1) + assert Fraction( + **report["below_first_cut"]["declared"]["weighted_amount_cents"] + ) == 3_999_999 + (4_000_000 if closure == "right" else 0) + assert report["cuts"][0]["cut"] == "first_lower_cut" + assert report["cuts"][0]["source_cut_cents"] == 4_000_000 + + +def test_nil_and_loss_records_follow_the_declared_negative_policy(): + assert _classify(_table(), [-123_456, 0, 1]) == ["nil", "nil", "b0"] + with pytest.raises(IntervalPreparationError) as refused: + classify_records( + interval_table(_table(policy="refuse")), [-1, 0, 5], ["included"] * 3 + ) + assert refused.value.summary["reasons"] == {"negative_amount_refused": 1} + # Excluded losses never enter a band under either policy. + assert _classify(_table(policy="refuse"), [-1], ["excluded"]) == ["excluded"] + + +@pytest.mark.parametrize("scaled", [False, True]) +def test_left_closed_nil_table_refuses_a_zero_first_cut(scaled): + table = _scale(_table(first=1)) if scaled else _table() + table["positive_band_closure"] = "left" + table["diagnostics"]["alternative_positive_band_closure"] = "right" + table["first_lower_cut_cents"] = {"numerator": 0, "denominator": 1} if scaled else 0 + with pytest.raises(ValueError, match="left-closed table with a nil band"): + interval_table(table) + + +def test_left_closed_nil_table_keeps_a_positive_fractional_first_cut_after_scaling(): + effective = _scale( + _table(closure="left", first=1), {"numerator": 1, "denominator": 2} + ) + assert effective["first_lower_cut_cents"] == {"numerator": 1, "denominator": 2} + assert _classify(effective, [-1, 0, 1]) == ["nil", "nil", "b0"] + + +def test_right_closed_nil_diagnostics_keep_zero_out_of_the_left_closed_alternative(): + table = interval_table(_table()) + report = boundary_diagnostics( + table, + [0, 1], + [-5, 0], + ["included", "included"], + [2.0, 3.0], + weight_record_ids=[0, 1], + weight_kind="design", + ) + bands = {band["band"]: band for band in report["bands"]} + assert bands["nil"]["declared"]["records"] == 2 + for band in table.band_ids[1:]: + assert bands[band]["declared"]["records"] == 0 + assert bands[band]["alternative"]["records"] == 0 + assert report["below_first_cut"]["alternative"]["records"] == 0 + + +@pytest.mark.parametrize( + ("closure", "nil", "first", "declared_destination", "alternative_destination"), + [ + ("right", True, 0, "nil", "nil"), + ("right", True, 10, "nil", "nil"), + ("left", True, 1, "nil", "nil"), + ("right", False, 1, "below_first_cut", "below_first_cut"), + ("left", False, 1, "below_first_cut", "below_first_cut"), + ("right", False, 0, "none", "b0"), + ("left", False, 0, "b0", "none"), + ], +) +def test_floored_negative_mass_reports_signed_amounts_by_actual_destination( + closure, nil, first, declared_destination, alternative_destination +): + report = boundary_diagnostics( + interval_table(_table(closure=closure, nil=nil, first=first)), + [0, 1, 2, 3, 4], + [-120, -30, -999, 0, 1], + ["included", "included", "excluded", "included", "included"], + [2.0, 0.5, 99.0, 3.0, 4.0], + weight_record_ids=[0, 1, 2, 3, 4], + weight_kind="design", + ) + floored = report["floored_negative_income"] + expected = { + "records": 2, + "weighted_count": {"numerator": 5, "denominator": 2}, + "pre_floor_amount_cents": -150, + "weighted_pre_floor_amount_cents": {"numerator": -255, "denominator": 1}, + } + assert {key: floored[key] for key in expected} == expected + for evaluation, destination in ( + ("declared", declared_destination), + ("alternative", alternative_destination), + ): + destinations = floored["by_destination"][evaluation] + actual = ( + destinations["positive_bands"][destination] + if destination == "b0" + else destinations[destination] + ) + assert actual == expected + all_destinations = [ + destinations["nil"], + destinations["below_first_cut"], + destinations["none"], + *destinations["positive_bands"].values(), + ] + assert sum(row["records"] for row in all_destinations) == 2 + assert sum(row["pre_floor_amount_cents"] for row in all_destinations) == -150 + for measure in ("weighted_count", "weighted_pre_floor_amount_cents"): + assert sum( + Fraction(**row[measure]) for row in all_destinations + ) == Fraction(**expected[measure]) + + +def test_a_band_id_cannot_merge_with_the_below_first_cut_diagnostic_bucket(): + declaration = _table(first=100) + declaration["bands"][1]["id"] = "below_first_cut" + report = boundary_diagnostics( + interval_table(declaration), + [0, 1], + [50, 101], + ["included", "included"], + [2.0, 3.0], + weight_record_ids=[0, 1], + weight_kind="design", + ) + band = next(row for row in report["bands"] if row["band"] == "below_first_cut") + assert band["declared"]["weighted_count"] == {"numerator": 3, "denominator": 1} + assert report["below_first_cut"]["declared"]["weighted_count"] == { + "numerator": 2, + "denominator": 1, + } + + +def test_fractional_cent_aged_cuts_stay_exact_and_are_never_rounded(): + effective = _scale(_table((10_000, 100_000, 4_000_000))) + uppers = [band.get("upper_cent") for band in effective["bands"]] + assert uppers == [ + None, + {"numerator": 1117109, "denominator": 100}, + {"numerator": 1117109, "denominator": 10}, + {"numerator": 4468436, "denominator": 1}, + None, + ] + assert Fraction(1117109, 100) == Fraction("11171.09") + assert Fraction(1117109, 10) == Fraction("111710.9") + # $111.7109: 11,171 cents is inside the first band and 11,172 above it. + # $1,117.109: 111,710 cents is inside, and 111,711 lies above the cut. + assert _classify(effective, [11_171, 11_172, 111_710, 111_711]) == [ + "b0", + "b1", + "b1", + "b2", + ] + assert _classify(effective, [4_468_436, 4_468_437]) == ["b2", "b3"] + assert effective["scaling"]["operational_factor"] == CENTRAL + assert source_interval_table(effective) == _table((10_000, 100_000, 4_000_000)) + + +def test_included_excluded_and_unresolved_routes(): + table = interval_table(_table()) + indicators = interval_indicators( + table, [0, 0, 500, 500_000], ["included", "excluded", "included", "excluded"] + ) + assert {name: values.tolist() for name, values in indicators.items()} == { + "table_nil": [True, False, False, False], + "table_b0": [False, False, True, False], + "table_b1": [False, False, False, False], + "table_b2": [False, False, False, False], + } + with pytest.raises(IntervalPreparationError) as refused: + interval_indicators( + table, [1, 2, 3, 4], ["included", "unresolved", "maybe", None] + ) + assert refused.value.summary == { + "table": "table", + "entity": "person", + "records": 4, + "refused_records": 3, + "reasons": {"invalid_universe_status": 2, "unresolved_universe_status": 1}, + "universe_statuses": {"included": 1, "invalid": 2, "unresolved": 1}, + } + + +@pytest.mark.parametrize( + ("amount", "reason"), + [ + (True, "boolean_amount"), + (np.bool_(False), "boolean_amount"), + (1.0, "non_integer_amount"), + (np.float64(250.0), "non_integer_amount"), + ("100", "text_amount"), + (None, "missing_amount"), + (float("nan"), "missing_amount"), + (float("inf"), "nonfinite_amount"), + (pd.NA, "missing_amount"), + (Decimal("100"), "non_integer_amount"), + (Fraction(1, 2), "non_integer_amount"), + ], +) +def test_booleans_floats_strings_and_missing_amounts_refuse(amount, reason): + table = interval_table(_table()) + with pytest.raises(IntervalPreparationError) as refused: + interval_indicators(table, [10, amount], ["included", "included"]) + assert refused.value.summary["reasons"] == {reason: 1} + # An excluded record's amount is never read. + assert interval_indicators(table, [10, amount], ["included", "excluded"])[ + "table_b0" + ].tolist() == [True, False] + + +def test_signed_numpy_and_python_integers_classify_identically(): + table = interval_table(_table()) + expected = classify_records(table, [-5, 0, 10_000], ["included"] * 3) + assert ( + classify_records( + table, list(np.asarray([-5, 0, 10_000], dtype=np.int64)), ["included"] * 3 + ) + == expected + ) + + +@pytest.mark.parametrize( + "field", + [ + "positive_band_closure", + "nil_band", + "negative_income_policy", + "first_lower_cut_cents", + "input_unit", + "income_column", + "universe_column", + "entity", + "currency", + "source_period", + "target_period", + "source_locator", + "diagnostics", + ], +) +def test_every_semantic_is_declared_and_none_has_a_default(field): + table = _table() + table.pop(field) + with pytest.raises(ValueError, match="missing declared fields"): + interval_table(table) + + +@pytest.mark.parametrize( + ("field", "value"), + [ + ("positive_band_closure", "both"), + ("positive_band_closure", True), + ("nil_band", True), + ("nil_band", "maybe"), + ("negative_income_policy", "absolute"), + ("input_unit", "float_dollar"), + ("first_lower_cut_cents", -1), + ("first_lower_cut_cents", 0.0), + ("first_lower_cut_cents", False), + ("source_period", True), + ("income_column", "universe_status"), + ], +) +def test_invalid_declared_semantics_refuse(field, value): + table = _table() + table[field] = value + with pytest.raises(ValueError): + interval_table(table) + + +def test_the_diagnostic_alternative_is_the_other_closure(): + table = _table(closure="left", first=1) + assert table["diagnostics"]["alternative_positive_band_closure"] == "right" + interval_table(table) + table["diagnostics"]["alternative_positive_band_closure"] = "left" + with pytest.raises(ValueError, match="other positive band closure"): + interval_table(table) + + +@pytest.mark.parametrize( + "cut", + [ + 10_000.0, + float("inf"), + float("nan"), + True, + "10000", + json.loads('{"x": Infinity}')["x"], + json.loads('{"x": NaN}')["x"], + -5, + 0, + ], +) +def test_floats_booleans_nonfinite_and_nonpositive_source_cuts_refuse(cut): + table = _table() + table["bands"][1]["upper_cent"] = cut + with pytest.raises(ValueError): + interval_table(table) + + +@pytest.mark.parametrize( + "rational", + [ + {"numerator": 2234218, "denominator": 200}, + {"numerator": 1117109, "denominator": 0}, + {"numerator": 1117109, "denominator": -100}, + {"numerator": 1117109.0, "denominator": 100}, + {"numerator": True, "denominator": 1}, + {"numerator": 1117109, "denominator": 100, "extra": 1}, + ], +) +def test_effective_cuts_must_be_reduced_integer_rationals(rational): + effective = _scale(_table()) + effective["bands"][1]["upper_cent"] = rational + with pytest.raises(ValueError): + interval_table(effective) + + +@pytest.mark.parametrize( + "defect", + [ + "decreasing", + "repeated", + "below_first", + "two_tails", + "tail_not_last", + "no_tail", + "nil_not_first", + "nil_declared_absent", + "nil_missing", + "duplicate_band", + "duplicate_indicator", + "unknown_kind", + "partial_companions", + "mixed_companions", + "unknown_field", + ], +) +def test_malformed_band_structures_refuse(defect): + table = _table((10_000, 100_000, 200_000)) + bands = table["bands"] + if defect == "decreasing": + bands[2]["upper_cent"] = 5_000 + elif defect == "repeated": + bands[2]["upper_cent"] = 10_000 + elif defect == "below_first": + table["first_lower_cut_cents"] = 10_000 + elif defect == "two_tails": + bands[2]["upper_cent"] = None + elif defect == "tail_not_last": + bands[-1], bands[-2] = bands[-2], bands[-1] + elif defect == "no_tail": + bands.pop() + elif defect == "nil_not_first": + bands.append(bands.pop(0)) + elif defect == "nil_declared_absent": + table["nil_band"] = "absent" + elif defect == "nil_missing": + bands.pop(0) + elif defect == "duplicate_band": + bands[2]["id"] = bands[1]["id"] + elif defect == "duplicate_indicator": + bands[2]["indicator"] = bands[1]["indicator"] + elif defect == "unknown_kind": + bands[1]["kind"] = "finite" + elif defect == "partial_companions": + bands[1]["count_reference"] = "count_1" + elif defect == "mixed_companions": + bands[1].update(count_reference="count_1", amount_reference="amount_1") + else: + bands[1]["lower_cent"] = 0 + with pytest.raises(ValueError): + interval_table(table) + + +def test_documents_require_unique_tables_indicators_and_references(): + first = _table(table_id="first") + second = _table(table_id="second", entity="household") + validate_interval_document({"schema_version": 1, "tables": [first, second]}) + for defect in ("table", "indicator", "reference", "schema", "unknown"): + tables = [copy.deepcopy(first), copy.deepcopy(second)] + document = {"schema_version": 1, "tables": tables} + if defect == "table": + tables[1]["id"] = "first" + elif defect == "indicator": + tables[1]["bands"][1]["indicator"] = tables[0]["bands"][1]["indicator"] + elif defect == "reference": + for table in tables: + for index, band in enumerate(table["bands"]): + band.update( + count_reference=f"{table['id']}_count_{index}", + amount_reference=f"{table['id']}_amount_{index}", + ) + tables[1]["bands"][1]["count_reference"] = "first_count_1" + elif defect == "schema": + document["schema_version"] = True + else: + document["defaults"] = {"positive_band_closure": "right"} + with pytest.raises(ValueError): + validate_interval_document(document) + + +def test_scaling_twice_refuses_and_scaled_cuts_bind_their_source(): + source = _table() + effective = _scale(source) + with pytest.raises(ValueError, match="already scaled"): + _scale(effective) + with pytest.raises(ValueError, match="cannot contain scaled tables"): + scale_interval_document( + {"schema_version": 1, "tables": [effective]}, {"table": {}} + ) + twice = copy.deepcopy(effective) + factor = Fraction(1117109, 1000000) + for band in twice["bands"]: + if band.get("upper_cent") is not None: + value = ( + Fraction(band["upper_cent"]["numerator"], 1) + / band["upper_cent"]["denominator"] + ) + value *= factor + band["upper_cent"] = { + "numerator": value.numerator, + "denominator": value.denominator, + } + with pytest.raises(ValueError, match="exactly once"): + interval_table(twice) + for mutation in ("source_cut", "factor", "source_hash", "operation", "missing"): + tampered = copy.deepcopy(effective) + if mutation == "source_cut": + tampered["bands"][1]["source_upper_cent"] = 10_001 + elif mutation == "factor": + tampered["scaling"]["operational_factor"] = { + "numerator": 1117110, + "denominator": 1000000, + } + elif mutation == "source_hash": + tampered["scaling"]["source_table_sha256"] = "0" * 64 + elif mutation == "operation": + tampered["scaling"]["operation"] = "round_to_cents" + else: + tampered["bands"][1].pop("source_upper_cent") + with pytest.raises(ValueError): + interval_table(tampered) + + +def test_document_scaling_changes_only_named_tables(): + document = { + "schema_version": 1, + "tables": [_table(table_id="scaled"), _table(table_id="kept")], + } + effective = scale_interval_document( + document, + { + "scaled": { + "factor": CENTRAL, + "factor_id": "toy_factor", + "factor_declaration_sha256": "a" * 64, + "factor_receipt_sha256": "b" * 64, + } + }, + ) + assert effective["tables"][1] == document["tables"][1] + assert "scaling" in effective["tables"][0] + assert interval_document_sha256(effective) != interval_document_sha256(document) + with pytest.raises(ValueError, match="Unknown interval tables"): + scale_interval_document(document, {"missing": {}}) + + +def test_input_fingerprints_keep_value_types(): + digests = { + interval_input_fingerprint(values) + for values in ([1, 2], [True, 2], [1.0, 2], ["1", 2], [np.int64(1), 2]) + } + assert len(digests) == 4 + assert interval_input_fingerprint([np.int64(1), 2]) == interval_input_fingerprint( + [1, 2] + ) + + +# --------------------------------------------------------------------------- +# Diagnostics on frames +# --------------------------------------------------------------------------- + + +def test_household_entity_table_is_weighted_through_memberships(): + table = interval_table( + _table(entity="household", income="household_income", universe="household_in") + ) + frame = Frame( + { + "person": pd.DataFrame( + {"person_id": [0, 1, 2, 3], "person_household_id": [0, 0, 1, 2]} + ), + "household": pd.DataFrame( + { + "household_id": [0, 1, 2], + "household_income": [0, 10_000, 10_001], + "household_in": ["included", "included", "excluded"], + } + ), + }, + EntitySchema(group_entities=("household",)), + {"person": Weights(np.asarray([2.0, 2.0, 3.0, 5.0]), WeightKind.CALIBRATED)}, + ) + report = frame_boundary_diagnostics(table, frame) + assert report["entity"] == "household" + assert report["weight_kind"] == "calibrated" + bands = {row["band"]: row for row in report["bands"]} + assert bands["nil"]["declared"]["weighted_count"] == { + "numerator": 2, + "denominator": 1, + } + assert bands["b0"]["declared"]["weighted_count"] == { + "numerator": 3, + "denominator": 1, + } + assert report["included_records"] == 2 + # The left-closed alternative moves the exact-cut household to the next band. + assert bands["b0"]["delta"]["weighted_count"] == { + "numerator": -3, + "denominator": 1, + } + assert bands["b1"]["delta"]["weighted_amount_cents"] == { + "numerator": 30_000, + "denominator": 1, + } + + +def test_group_entity_without_weights_resolves_from_the_weighted_household(): + table = interval_table( + _table(entity="tax_unit", income="unit_income", universe="unit_in") + ) + frame = Frame( + { + "person": pd.DataFrame( + { + "person_id": [0, 1, 2], + "person_household_id": [0, 0, 1], + "person_tax_unit_id": [0, 1, 2], + } + ), + "household": pd.DataFrame({"household_id": [0, 1]}), + "tax_unit": pd.DataFrame( + { + "tax_unit_id": [0, 1, 2], + "unit_income": [5, 0, 200_000], + "unit_in": ["included", "excluded", "included"], + } + ), + }, + EntitySchema(group_entities=("household", "tax_unit")), + {"household": Weights(np.asarray([4.0, 6.0]), WeightKind.DESIGN)}, + ) + report = frame_boundary_diagnostics(table, frame) + bands = {row["band"]: row for row in report["bands"]} + assert bands["b0"]["declared"]["weighted_count"]["numerator"] == 4 + assert bands["b2"]["declared"]["weighted_amount_cents"]["numerator"] == 1_200_000 + assert report["weight_kind"] == "design" + + +def test_diagnostics_report_near_cut_mass_digests_and_disclosure(): + effective = interval_table(_scale(_table())) + amounts = [0, 11_071, 11_072, 11_271, 11_272, 111_711] + weights = [1.0, 0.5, 0.25, 0.125, 2.0, 3.0] + report = boundary_diagnostics( + effective, + list(range(6)), + amounts, + ["included"] * 6, + weights, + weight_record_ids=list(range(6)), + weight_kind="design", + ) + near = {row["cut"]: row for row in report["cuts"]} + # |z - 11,171.09| <= 100 cents admits 11,072 and 11,271 only. + assert near["b0"]["near_cut_records"] == 2 + assert Fraction(**near["b0"]["near_cut_weighted_count"]) == Fraction(3, 8) + assert near["b0"]["cut_cents"] == {"numerator": 1117109, "denominator": 100} + assert near["b0"]["source_cut_cents"] == 10_000 + assert near["first_lower_cut"]["near_cut_records"] == 1 + assert report["publisher_reclassification"] == "unavailable" + assert "cannot be recovered" in report["limitation"] + assert report["digests"]["factor"]["operational_factor"] == CENTRAL + assert set(report["digests"]) == { + "table_sha256", + "source_cuts_sha256", + "effective_cuts_sha256", + "factor", + "weights_sha256", + "record_order_sha256", + } + assert ( + report["digests"]["source_cuts_sha256"] + != report["digests"]["effective_cuts_sha256"] + ) + + +def test_misaligned_weights_and_nonfinite_weights_refuse(): + table = interval_table(_table()) + arguments = dict( + record_ids=[0, 1], + amounts=[1, 2], + universe=["included", "included"], + weight_kind="design", + ) + with pytest.raises(ValueError, match="aligned"): + boundary_diagnostics( + table, weights=[1.0, 2.0], weight_record_ids=[1, 0], **arguments + ) + with pytest.raises(ValueError, match="finite"): + boundary_diagnostics( + table, weights=[1.0, float("nan")], weight_record_ids=[0, 1], **arguments + ) + + +# --------------------------------------------------------------------------- +# Hypothesis: an independent Fraction oracle +# --------------------------------------------------------------------------- + + +def _oracle_cut(value) -> Fraction: + if isinstance(value, dict): + return Fraction(value["numerator"], value["denominator"]) + return Fraction(value) + + +def _oracle_cuts(table) -> list[Fraction]: + cuts = [_oracle_cut(table["first_lower_cut_cents"])] + for band in table["bands"]: + if band["kind"] == "positive" and band["upper_cent"] is not None: + cuts.append(_oracle_cut(band["upper_cent"])) + return cuts + + +def _oracle_band(z: Fraction, table) -> str: + """Band id for a nonnegative income under the table's own declaration.""" + if table["nil_band"] == "present" and z == 0: + return "nil" + cuts = _oracle_cuts(table) + positive = [band["id"] for band in table["bands"] if band["kind"] == "positive"] + for index, band_id in enumerate(positive): + lower = cuts[index] + upper = cuts[index + 1] if index + 1 < len(cuts) else None + if table["positive_band_closure"] == "right": + inside = lower < z and (upper is None or z <= upper) + else: + inside = lower <= z and (upper is None or z < upper) + if inside: + return band_id + return "below" + + +@st.composite +def _tables(draw, big=False): + count = draw(st.integers(min_value=1, max_value=6)) + step = st.integers(min_value=1, max_value=10**30 if big else 50_000) + closure = draw(st.sampled_from(["left", "right"])) + nil = draw(st.booleans()) + first = draw(step if nil and closure == "left" else st.one_of(st.just(0), step)) + cuts, previous = [], first + for _ in range(count): + previous += draw(step) + cuts.append(previous) + return _table( + tuple(cuts), + closure=closure, + nil=nil, + first=first, + ) + + +_FACTORS = st.builds( + lambda numerator, denominator: { + "numerator": numerator, + "denominator": denominator, + }, + st.integers(min_value=1, max_value=10**9), + st.one_of( + st.sampled_from([1, 10, 100, 1_000_000]), + st.integers(min_value=1, max_value=10**9), + ), +) + + +def _near_cuts(table, draw, count): + cuts = [int(cut) for cut in _oracle_cuts(table)] + values = st.one_of( + st.integers(min_value=-(10**6), max_value=cuts[-1] * 2 + 10), + st.sampled_from(cuts).flatmap( + lambda cut: st.integers(min_value=cut - 2, max_value=cut + 2) + ), + ) + return draw(st.lists(values, min_size=1, max_size=count)) + + +@given(data=st.data()) +@_SETTINGS +def test_property_included_records_fall_in_exactly_one_band_excluded_in_none(data): + table = data.draw(_tables()) + amounts = _near_cuts(table, data.draw, 24) + universe = data.draw( + st.lists( + st.sampled_from(["included", "excluded"]), + min_size=len(amounts), + max_size=len(amounts), + ) + ) + parsed = interval_table(table) + indicators = interval_indicators(parsed, amounts, universe) + stacked = np.vstack(list(indicators.values())) + for index, (amount, status) in enumerate(zip(amounts, universe, strict=True)): + column = stacked[:, index] + expected = _oracle_band(Fraction(max(0, amount)), table) + if status == "excluded": + assert not column.any() + elif expected == "below": + assert not column.any() + assert _oracle_cuts(table)[0] > 0 or ( + table["nil_band"] == "absent" + and table["positive_band_closure"] == "right" + and amount <= 0 + ) + else: + assert column.sum() == 1 + assert parsed.band_ids[int(np.argmax(column))] == expected + + +@given(data=st.data()) +@_SETTINGS +def test_property_unresolved_records_never_yield_indicators(data): + table = data.draw(_tables()) + amounts = _near_cuts(table, data.draw, 12) + universe = data.draw( + st.lists( + st.sampled_from(["included", "excluded", "unresolved"]), + min_size=len(amounts), + max_size=len(amounts), + ) + ) + unresolved = universe.count("unresolved") + if not unresolved: + universe[data.draw(st.integers(0, len(universe) - 1))] = "unresolved" + unresolved = 1 + with pytest.raises(IntervalPreparationError) as refused: + interval_indicators(interval_table(table), amounts, universe) + assert refused.value.summary["reasons"]["unresolved_universe_status"] == unresolved + + +@given(table=_tables(), factor=_FACTORS) +@_SETTINGS +def test_property_scaling_preserves_order_nil_closure_and_tails(table, factor): + effective = _scale(table, factor) + rational = Fraction(factor["numerator"], factor["denominator"]) + assert [_oracle_cut(cut) for cut in [effective["first_lower_cut_cents"]]] == [ + Fraction(table["first_lower_cut_cents"]) * rational + ] + assert _oracle_cuts(effective) == [cut * rational for cut in _oracle_cuts(table)] + cuts = _oracle_cuts(effective) + assert all(left < right for left, right in zip(cuts, cuts[1:], strict=False)) + for before, after in zip(table["bands"], effective["bands"], strict=True): + assert (before["id"], before["kind"], before["indicator"]) == ( + after["id"], + after["kind"], + after["indicator"], + ) + assert effective["bands"][-1]["upper_cent"] is None + for key in ("positive_band_closure", "nil_band", "negative_income_policy"): + assert effective[key] == table[key] + with pytest.raises(ValueError, match="already scaled"): + _scale(effective, factor) + + +@given(data=st.data(), factor=_FACTORS) +@_SETTINGS +def test_property_scaled_classification_equals_deflated_source_classification( + data, factor +): + table = data.draw(_tables()) + effective = _scale(table, factor) + rational = Fraction(factor["numerator"], factor["denominator"]) + scaled_cuts = _oracle_cuts(effective) + amounts = data.draw( + st.lists( + st.one_of( + st.integers(min_value=-(10**6), max_value=int(scaled_cuts[-1]) * 2 + 2), + st.sampled_from(scaled_cuts).flatmap( + lambda cut: st.integers( + min_value=int(cut) - 2, max_value=int(cut) + 2 + ) + ), + ), + min_size=1, + max_size=24, + ) + ) + actual = _classify(effective, amounts) + expected = [ + _oracle_band(Fraction(max(0, amount)) / rational, table) for amount in amounts + ] + assert actual == expected + + +@given(table=_tables(), factor=_FACTORS) +@_SETTINGS +def test_property_adjacent_attainable_cents_around_fractional_cuts(table, factor): + effective = _scale(table, factor) + for cut in _oracle_cuts(effective): + floor = cut.numerator // cut.denominator + amounts = [floor - 1, floor, floor + 1, floor + 2] + actual = _classify(effective, amounts) + assert actual == [ + _oracle_band(Fraction(max(0, amount)), effective) for amount in amounts + ] + if cut.denominator != 1: + # No attainable cent sits on a fractional cut: floor and floor + 1 + # straddle it under either closure. + assert floor < cut < floor + 1 + + +@given(data=st.data()) +@_SETTINGS +def test_property_permuting_records_and_weights_preserves_diagnostics(data): + table = data.draw(_tables()) + amounts = _near_cuts(table, data.draw, 16) + universe = data.draw( + st.lists( + st.sampled_from(["included", "excluded"]), + min_size=len(amounts), + max_size=len(amounts), + ) + ) + weights = data.draw( + st.lists( + st.floats(min_value=0, max_value=1e6, allow_nan=False), + min_size=len(amounts), + max_size=len(amounts), + ) + ) + ids = list(range(len(amounts))) + order = data.draw(st.permutations(ids)) + parsed = interval_table(table) + + def report(indices, weight_ids=None): + rows = [ids[index] for index in indices] + return boundary_diagnostics( + parsed, + rows, + [amounts[index] for index in indices], + [universe[index] for index in indices], + [weights[index] for index in indices], + weight_record_ids=rows if weight_ids is None else weight_ids, + weight_kind="design", + ) + + original, permuted = report(ids), report(order) + for key in ( + "cuts", + "bands", + "below_first_cut", + "floored_negative_income", + "included_records", + ): + assert original[key] == permuted[key] + if order != ids: + with pytest.raises(ValueError, match="aligned"): + report(order, weight_ids=ids) + + +@given(data=st.data()) +@_SETTINGS +def test_property_alternative_closure_deltas_sum_to_zero_and_nil_is_unchanged(data): + table = data.draw(_tables()) + amounts = _near_cuts(table, data.draw, 20) + weights = data.draw( + st.lists( + st.floats(min_value=0, max_value=1e9, allow_nan=False), + min_size=len(amounts), + max_size=len(amounts), + ) + ) + ids = list(range(len(amounts))) + report = boundary_diagnostics( + interval_table(table), + ids, + amounts, + ["included"] * len(amounts), + weights, + weight_record_ids=ids, + weight_kind="design", + ) + rows = [row for row in report["bands"] if row["kind"] == "positive"] + rows.append(report["below_first_cut"]) + for measure in ("weighted_count", "weighted_amount_cents"): + assert sum(Fraction(**row["delta"][measure]) for row in rows) == 0 + assert sum(row["delta"]["records"] for row in rows) == 0 + nil = [row for row in report["bands"] if row["kind"] == "nil"] + assert all(set(row) == {"band", "kind", "declared"} for row in nil) + # The oracle's exact weighted count per declared band. + for row in report["bands"]: + expected = sum( + ( + Fraction(weight) + for amount, weight in zip(amounts, weights, strict=True) + if _oracle_band(Fraction(max(0, amount)), table) == row["band"] + ), + Fraction(0), + ) + assert Fraction(**row["declared"]["weighted_count"]) == expected + + +@given(data=st.data()) +@_SETTINGS +def test_property_very_large_cross_products_do_not_overflow(data): + table = data.draw(_tables(big=True)) + factor = data.draw( + st.builds( + lambda numerator, denominator: { + "numerator": numerator, + "denominator": denominator, + }, + st.integers(min_value=1, max_value=10**40), + st.integers(min_value=1, max_value=10**40), + ) + ) + effective = _scale(table, factor) + cuts = _oracle_cuts(effective) + amounts = data.draw( + st.lists( + st.one_of( + st.integers(min_value=-(10**45), max_value=10**75), + st.sampled_from(cuts).flatmap( + lambda cut: st.integers( + min_value=int(cut) - 1, max_value=int(cut) + 1 + ) + ), + ), + min_size=1, + max_size=12, + ) + ) + rational = Fraction(factor["numerator"], factor["denominator"]) + assert _classify(effective, amounts) == [ + _oracle_band(Fraction(max(0, amount)) / rational, table) for amount in amounts + ] diff --git a/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py b/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py index 60e2cee84d..0f05ff89c3 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py @@ -9,8 +9,9 @@ import pytest from hypothesis import HealthCheck, given, settings from hypothesis import strategies as st -from microcosm_build.reference_factors import factor_context_fixture +from microcosm_build.reference_factors import factor_context_fixture, factor_fixture +from microcosm.build.reference_factors import resolve_reference_factor from microcosm.build.reference_profiles import ( REFERENCE_DOCUMENT_KEYS, reference_document_sha256, @@ -545,14 +546,326 @@ def test_invalid_factor_profiles_and_companions_refuse(mutation): resolve_reference_profiles(references, declarations, {}, factor_context=context) -def test_nonempty_interval_roster_refuses_until_interval_implementation(): - references, declarations, _ = _scaling_inputs() +def _interval_document(pair_count=2, *, table_id="reviewed_table", companions=True): + bands = [] + for index in range(pair_count): + band = { + "id": f"band_{index}", + "kind": "positive", + "upper_cent": None if index == pair_count - 1 else 10_000 * (index + 1), + "indicator": f"band_{index}", + } + if companions: + band.update( + count_reference=f"count_{index}", amount_reference=f"money_{index}" + ) + bands.append(band) + return { + "schema_version": 1, + "tables": [ + { + "id": table_id, + "entity": "household", + "currency": "XTS", + "input_unit": "integer_cent", + "income_column": "household_income_cents", + "universe_column": "household_income_universe", + "source_period": 2001, + "target_period": 2002, + "positive_band_closure": "right", + "nil_band": "absent", + "negative_income_policy": "floor_zero", + "first_lower_cut_cents": 0, + "source_locator": "Toy!A1:A2", + "diagnostics": { + "boundary_window_cents": 100, + "alternative_positive_band_closure": "left", + }, + "bands": bands, + } + ], + } + + +def _interval_inputs(pair_count=2): + references, declarations, context = _scaling_inputs(pair_count) + for profile in declarations["dimensions"]["aging_index"]["profiles"].values(): + profile["transformations"][0]["interval_table_ids"] = ["reviewed_table"] + return references, declarations, context, _interval_document(pair_count) + + +def _effective_cuts(intervals, table_id="reviewed_table"): + from fractions import Fraction + + table = next(table for table in intervals["tables"] if table["id"] == table_id) + return [ + None + if band["upper_cent"] is None + else Fraction( + band["upper_cent"]["numerator"], band["upper_cent"]["denominator"] + ) + for band in table["bands"] + ] + + +def test_named_interval_tables_scale_once_with_their_money_factor(): + references, declarations, context, intervals = _interval_inputs(3) + original = copy.deepcopy((references, declarations, context, intervals)) + effective, effective_intervals, receipt = resolve_reference_profiles( + references, declarations, {}, intervals, factor_context=context + ) + assert (references, declarations, context, intervals) == original + rational = context["factors"][0]["operational_factor"] + factor = rational["numerator"] / rational["denominator"] + # One factor covers each money/cut triple. + money = [ + row + for row in effective["target_references"] + if row["name"] in {"money_0", "money_1", "money_2"} + ] + assert {row["value_operands"][0]["weight"] for row in money} == {factor} + assert _effective_cuts(effective_intervals) == [11_000, 22_000, None] + table = effective_intervals["tables"][0] + assert table["scaling"]["factor_id"] == "toy_factor" + assert table["scaling"]["operational_factor"] == rational + assert [band.get("source_upper_cent") for band in table["bands"]] == [ + 10_000, + 20_000, + None, + ] + assert receipt["base_intervals_sha256"] == reference_document_sha256(intervals) + assert receipt["effective_intervals_sha256"] == reference_document_sha256( + effective_intervals + ) + assert receipt["effective_intervals_sha256"] != receipt["base_intervals_sha256"] + assert receipt["interval_tables"]["reviewed_table"]["factor_id"] == "toy_factor" + assert receipt["interval_tables_sha256"] == reference_document_sha256( + receipt["interval_tables"] + ) + assert receipt["receipt_sha256"] not in json.dumps(effective_intervals) + for row in effective["target_references"]: + assert ( + row["metadata"]["effective_intervals_sha256"] + == (receipt["effective_intervals_sha256"]) + ) + if row["metadata"]["reference_value_kind"] == "count": + source = next( + item + for item in references["target_references"] + if item["name"] == row["name"] + ) + assert row["value_operation"] == source["value_operation"] + assert row["value_operands"] == source["value_operands"] + assert ( + validate_reference_resolution( + effective, receipt, interval_document=effective_intervals + ) + == receipt + ) + with pytest.raises(ValueError, match="interval document hash mismatch"): + validate_reference_resolution(effective, receipt, interval_document=intervals) + with pytest.raises(ValueError, match="already been resolved"): + resolve_reference_profiles( + effective, declarations, {}, effective_intervals, factor_context=context + ) + with pytest.raises(ValueError, match="cannot contain scaled tables"): + resolve_reference_profiles( + references, declarations, {}, effective_intervals, factor_context=context + ) + + +@pytest.mark.parametrize( + "mutation", + [ + "missing_document", + "unknown_table", + "duplicate_table_id", + "omitted_band", + "extra_band", + "cross_pair", + "swapped_roles", + "duplicate_reference", + "wrong_filter", + "wrong_entity", + "no_companions", + "source_period", + "target_period", + "second_transformation", + ], +) +def test_interval_companions_must_be_exactly_the_scaled_rosters(mutation): + references, declarations, context, intervals = _interval_inputs(3) transform = declarations["dimensions"]["aging_index"]["profiles"]["central"][ "transformations" ][0] - transform["interval_table_ids"] = ["reviewed_table"] - with pytest.raises(ValueError, match="interval scaling lands in r2b"): - resolve_reference_profiles(references, declarations, {}) + table = intervals["tables"][0] + bands = table["bands"] + if mutation == "missing_document": + intervals = None + elif mutation == "unknown_table": + transform["interval_table_ids"] = ["missing_table"] + elif mutation == "duplicate_table_id": + transform["interval_table_ids"] = ["reviewed_table", "reviewed_table"] + elif mutation == "omitted_band": + bands.pop(1) + elif mutation == "extra_band": + bands.insert( + 2, + { + "id": "band_extra", + "kind": "positive", + "upper_cent": 30_000, + "indicator": "band_extra", + "count_reference": "other_count", + "amount_reference": "other_money", + }, + ) + elif mutation == "cross_pair": + bands[0]["amount_reference"], bands[1]["amount_reference"] = ( + bands[1]["amount_reference"], + bands[0]["amount_reference"], + ) + elif mutation == "swapped_roles": + bands[0]["amount_reference"], bands[0]["count_reference"] = ( + bands[0]["count_reference"], + bands[0]["amount_reference"], + ) + elif mutation == "duplicate_reference": + bands[1]["count_reference"] = bands[0]["count_reference"] + elif mutation == "wrong_filter": + bands[0]["indicator"] = "another_indicator" + elif mutation == "wrong_entity": + table["entity"] = "person" + elif mutation == "no_companions": + intervals = _interval_document(3, companions=False) + elif mutation in {"source_period", "target_period"}: + table[mutation] = 1999 + else: + declarations["dimensions"]["another"] = { + "default": "only", + "profiles": {"only": {"transformations": [copy.deepcopy(transform)]}}, + } + with pytest.raises(ValueError): + resolve_reference_profiles( + references, declarations, {}, intervals, factor_context=context + ) + + +def test_tables_without_companions_validate_but_profiles_do_not_scale_them(): + references, declarations, context, intervals = _interval_inputs(2) + unscaled = _interval_document(3, table_id="declared_only", companions=False)[ + "tables" + ][0] + for band in unscaled["bands"]: + band["indicator"] = f"declared_{band['indicator']}" + intervals["tables"].append(unscaled) + _, effective_intervals, receipt = resolve_reference_profiles( + references, declarations, {}, intervals, factor_context=context + ) + assert effective_intervals["tables"][1] == unscaled + assert set(receipt["interval_tables"]) == {"reviewed_table"} + + +def test_unnamed_interval_tables_pass_through_and_effective_equals_base(): + references, declarations, context = _scaling_inputs() + intervals = _interval_document(2) + _, effective_intervals, receipt = resolve_reference_profiles( + references, declarations, {}, intervals, factor_context=context + ) + assert effective_intervals == intervals + assert "interval_tables" not in receipt + assert receipt["effective_intervals_sha256"] == receipt["base_intervals_sha256"] + + +def test_interval_scaling_is_local_to_the_selected_profile(): + references, declarations, context, intervals = _interval_inputs(2) + declaration, facts, identity = factor_fixture(anchor_level="105") + declaration["id"] = "toy_low" + factor, factor_receipt = resolve_reference_factor(declaration, facts, identity) + context["factors"].append( + { + "declaration": declaration, + "operational_factor": factor, + "receipt": factor_receipt, + } + ) + profiles = declarations["dimensions"]["aging_index"]["profiles"] + profiles["low"]["transformations"][0]["factor_id"] = "toy_low" + low = resolve_reference_profiles( + references, + declarations, + {"aging_index": "low"}, + intervals, + factor_context=context, + ) + central = resolve_reference_profiles( + references, declarations, {}, intervals, factor_context=context + ) + assert _effective_cuts(low[1]) == [10_500, None] + assert _effective_cuts(central[1]) == [11_000, None] + profiles["high"]["notes"] = "edited unselected high declaration" + assert ( + resolve_reference_profiles( + references, + declarations, + {"aging_index": "low"}, + intervals, + factor_context=context, + ) + == low + ) + assert ( + resolve_reference_profiles( + references, declarations, {}, intervals, factor_context=context + ) + == central + ) + + +@pytest.mark.parametrize( + "mutation", + [ + "record_factor", + "record_profile", + "record_hash", + "drop_records", + "document_table", + "unscaled_document", + ], +) +def test_rehashed_interval_scaling_records_still_refuse(mutation): + references, declarations, context, intervals = _interval_inputs(2) + effective, effective_intervals, receipt = resolve_reference_profiles( + references, declarations, {}, intervals, factor_context=context + ) + receipt = copy.deepcopy(receipt) + document = copy.deepcopy(effective_intervals) + record = receipt["interval_tables"]["reviewed_table"] + if mutation == "record_factor": + record["operational_factor"] = {"numerator": 1, "denominator": 1} + receipt["interval_tables_sha256"] = reference_document_sha256( + receipt["interval_tables"] + ) + elif mutation == "record_profile": + record["profile"] = "high" + receipt["interval_tables_sha256"] = reference_document_sha256( + receipt["interval_tables"] + ) + elif mutation == "record_hash": + record["effective_table_sha256"] = "0" * 64 + elif mutation == "drop_records": + receipt.pop("interval_tables") + receipt.pop("interval_tables_sha256") + elif mutation == "document_table": + document["tables"][0]["scaling"]["factor_receipt_sha256"] = "0" * 64 + receipt["effective_intervals_sha256"] = reference_document_sha256(document) + else: + document = copy.deepcopy(intervals) + receipt["effective_intervals_sha256"] = reference_document_sha256(document) + receipt.pop("receipt_sha256") + receipt["receipt_sha256"] = reference_document_sha256(receipt) + with pytest.raises(ValueError): + validate_reference_resolution(effective, receipt, interval_document=document) def test_factor_evidence_and_unselected_profile_locality(): @@ -714,8 +1027,13 @@ def test_nz_default_factor_receipt_is_compact_and_covers_the_exact_roster(): base = json.loads((spec / "target_references.json").read_text()) declarations = json.loads((spec / "reference_profiles.json").read_text()) context = json.loads((spec / "reference_aging.json").read_text()) + intervals = json.loads((spec / "reference_intervals.json").read_text()) _, _, receipt = resolve_reference_profiles( - base, declarations, {}, factor_context=context + base, declarations, {}, intervals, factor_context=context + ) + assert set(receipt["interval_tables"]) == {"ird_taxable_income_national_ty2024"} + assert ( + receipt["interval_tables"]["ird_taxable_income_national_ty2024"]["bands"] == 237 ) assert "selected_declarations" not in receipt assert "base_reference_document" not in receipt @@ -761,11 +1079,17 @@ def today(cls): base = json.loads((spec / "target_references.json").read_text()) declarations = json.loads((spec / "reference_profiles.json").read_text()) context = json.loads((spec / "reference_aging.json").read_text()) - effective, _, receipt = resolve_reference_profiles( - base, declarations, {}, factor_context=context + intervals = json.loads((spec / "reference_intervals.json").read_text()) + effective, effective_intervals, receipt = resolve_reference_profiles( + base, declarations, {}, intervals, factor_context=context ) assert receipt["selected_factors"] - assert validate_reference_resolution(effective, receipt) == receipt + assert ( + validate_reference_resolution( + effective, receipt, interval_document=effective_intervals + ) + == receipt + ) def test_unrelated_direct_fact_references_are_outside_companion_rosters(): diff --git a/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_country_bundles.py b/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_country_bundles.py index 981c214de6..e7a2b0e2a6 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_country_bundles.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_country_bundles.py @@ -234,6 +234,7 @@ def test_nz_generation_zero_views_come_from_the_country_spec_seam() -> None: "holdout_references.json", "precal_references.json", "reference_aging.json", + "reference_intervals.json", "reference_inventory.json", "reference_profiles.json", "release_contract.json", diff --git a/packages/microcosm-build/tests/engine_free/shared/test_transport_composed_graph.py b/packages/microcosm-build/tests/engine_free/shared/test_transport_composed_graph.py index 4187358f15..469d9352b0 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_transport_composed_graph.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_transport_composed_graph.py @@ -48,6 +48,7 @@ from unittest.mock import Mock import numpy as np +import pandas as pd import pytest from hypothesis import HealthCheck, given, settings from hypothesis import strategies as st @@ -71,6 +72,7 @@ ) from microcosm.build.transport.population_kernels import TRANSPORT_CURRENCY from microcosm.build.transport.takeup_kernels import GAP_TYPE +from microcosm.build.transport.target_kernels import CALIBRATION_PREPARE from microcosm.build.transport.terminal_kernels import materialize_export from microcosm.frame.kernels import SimulateRulesKernel from microcosm.graph import ( @@ -81,6 +83,7 @@ Determinism, GraphError, KernelBase, + KernelContext, KernelRegistry, Node, NodeRejectedError, @@ -98,6 +101,12 @@ from test_support.microcosm_build.reference_factor_transport import ( factor_transport_fixture, ) +from test_support.microcosm_build.reference_intervals import ( + INCOME_COLUMN, + UNIVERSE_COLUMN, + interval_bindings, + interval_profile_fixture, +) from test_support.microcosm_build.transport_composed import ( FAMILY_INPUTS, PROGRAMS, @@ -1105,6 +1114,167 @@ def test_declared_ageing_scenarios_bind_selected_evidence_and_preserve_locality( assert revised == receipts[scenario["id"]] +def _interval_prepare_spec(): + base, profiles, context, intervals = interval_profile_fixture() + table = intervals["tables"][0] + spec = { + "country": "xx", + "resources": { + "target_references": base, + "reference_profiles": profiles, + "factor_context": context, + "profile_intervals": intervals, + "calibration_bindings": interval_bindings(table), + }, + } + row = { + "id": "prepare", + "kernel": "calibration.prepare@1", + "population": "open", + "params": { + "bindings": {"resource": "calibration_bindings", "encoding": "json"}, + "references": {"resource": "target_references", "encoding": "json"}, + "references_sha256": { + "resource": "target_references", + "encoding": "sha256", + }, + }, + "outputs": [ + {"entity": "person", "column": band["indicator"], "dtype": "bool"} + for band in table["bands"] + ], + } + return spec, row + + +_INTERVAL_CONFIG = TransportGraphConfig( + engine_refs={}, create_outputs=(Owned("person", "age"),) +) + + +def _prepare_node(spec, row, knobs, *, interval_knobs="same"): + params = { + name: compose_module._resource_param(spec, value, profile_selection=knobs) + for name, value in row["params"].items() + } + if interval_knobs != "same": + params["intervals"] = compose_module._resource_param( + spec, + {"resource": "profile_intervals", "encoding": "json"}, + profile_selection=interval_knobs, + ) + return compose_module._node( + spec, + row, + _INTERVAL_CONFIG, + {}, + resolved_params=params, + profile_selection=knobs, + ) + + +def _run_prepare(node, incomes): + tables = { + "person": pd.DataFrame( + { + "person_id": range(len(incomes)), + INCOME_COLUMN: incomes, + UNIVERSE_COLUMN: ["included"] * len(incomes), + } + ) + } + return CALIBRATION_PREPARE.run( + KernelContext( + node=node, + tables=tables, + weights={}, + strata=pd.Series([], dtype=object, name="stratum"), + params=node.params, + rng=np.random.default_rng(0), + sources={}, + artifacts={}, + ) + ) + + +def test_composition_supplies_each_variant_its_own_effective_interval_companion(): + spec, row = _interval_prepare_spec() + incomes = [104_999, 107_000, 112_000, 114_000, 116_000] + first_band = {} + for profile, knobs in ( + ("central", None), + ("low", {"index": "low"}), + ("high", {"index": "high"}), + ): + node = _prepare_node(spec, row, knobs) + view = compose_module.reference_profile_view(spec, knobs) + assert node.params["intervals"] == canonical_json(view[1]).decode("utf-8") + assert json.loads(node.params["reference_resolution"]) == view[2] + result = _run_prepare(node, incomes) + first_band[profile] = result.columns["person", "toy_income_band_b0"].tolist() + evidence = json.loads(result.artifacts["preparation"]) + assert ( + evidence["effective_intervals_sha256"] + == view[2]["effective_intervals_sha256"] + ) + # 100,000 source cents become 105,000, 110,000 and 115,000: each variant + # regenerates its indicators from its own cuts rather than reusing S0's. + assert first_band == { + "low": [True, False, False, False, False], + "central": [True, True, False, False, False], + "high": [True, True, True, True, False], + } + + +def test_composition_refuses_a_supplied_interval_document_from_another_profile(): + spec, row = _interval_prepare_spec() + row = copy.deepcopy(row) + row["params"]["intervals"] = {"resource": "profile_intervals", "encoding": "json"} + node = _prepare_node(spec, row, {"index": "low"}) + assert ( + json.loads(node.params["intervals"])["tables"][0]["scaling"]["factor_id"] + == "toy_low" + ) + with pytest.raises(ValueError, match="differs from its effective interval"): + _prepare_node(spec, row, {"index": "low"}, interval_knobs=None) + + +def test_declared_scenarios_scale_national_cuts_with_their_own_factor(): + from fractions import Fraction + + spec = load_transport_spec("nz") + source = spec["resources"]["reference_intervals"] + by_id = {table["id"]: table for table in source["tables"]} + seen = {} + for scenario in spec["resources"]["scenarios"]["scenarios"]: + _, intervals, receipt = compose_module.reference_profile_view( + spec, scenario["knobs"] + ) + profile = {"V1": "low", "V2": "high"}.get(scenario["id"], "central") + tables = {table["id"]: table for table in intervals["tables"]} + national = tables["ird_taxable_income_national_ty2024"] + assert national["scaling"]["factor_id"] == f"ird_ty2024_to_ty2027_{profile}" + assert ( + tables["ird_taxable_income_age_ty2024"] + == by_id["ird_taxable_income_age_ty2024"] + ) + factor = national["scaling"]["operational_factor"] + rational = Fraction(factor["numerator"], factor["denominator"]) + for band, original in zip( + national["bands"], + by_id["ird_taxable_income_national_ty2024"]["bands"], + strict=True, + ): + if original.get("upper_cent") is not None: + upper = band["upper_cent"] + assert Fraction(upper["numerator"], upper["denominator"]) == ( + original["upper_cent"] * rational + ) + seen.setdefault(profile, set()).add(receipt["effective_intervals_sha256"]) + assert all(len(hashes) == 1 for hashes in seen.values()) + assert len(set().union(*seen.values())) == 3 + + class _IllegalOwner(KernelBase): ref = "test.illegal_owner@1" capabilities = Capabilities(Determinism.DETERMINISTIC) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_transport_target_kernels.py b/packages/microcosm-build/tests/engine_free/shared/test_transport_target_kernels.py index e72a10d902..10c2d3c7df 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_transport_target_kernels.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_transport_target_kernels.py @@ -31,6 +31,7 @@ from hypothesis import strategies as st import microcosm.build.reference_factors as reference_factors_module +import microcosm.build.reference_intervals as reference_intervals_module import microcosm.build.reference_profiles as reference_profiles_module import microcosm.build.transport.target_kernels as target_kernels_module from microcosm.build.ledger_artifact import ChronicleFeedPinMismatchError @@ -70,6 +71,12 @@ from test_support.microcosm_build.reference_factor_transport import ( factor_transport_fixture, ) +from test_support.microcosm_build.reference_intervals import ( + INCOME_COLUMN, + UNIVERSE_COLUMN, + interval_bindings, + interval_table_fixture, +) from test_support.microcosm_build.transport_graph import ( CALIBRATION_TARGETS, COUNTRY, @@ -884,10 +891,15 @@ def test_surface_decode_refuses_a_tampered_trace_or_registry(tmp_path) -> None: ) +# Base/current seeded draws have identical inputs and measured generation cost. +# Host contention can exceed Hypothesis' wall-clock generation health check. @settings( max_examples=40, deadline=None, - suppress_health_check=(HealthCheck.function_scoped_fixture,), + suppress_health_check=( + HealthCheck.function_scoped_fixture, + HealthCheck.too_slow, + ), ) @given( chosen=st.lists( @@ -1104,3 +1116,257 @@ def test_property_comparison_reports_the_fact_and_the_weighted_model( assert row["reference"] == row["name"] == HOLDOUT_TARGETS[0][0] assert document["weight_kind"] == "calibrated" assert result.receipt["n_comparisons"] == 1 + + +# --------------------------------------------------------------------------- +# Interval preparation and pre-fit boundary diagnostics +# --------------------------------------------------------------------------- + + +def _interval_document(): + return { + "schema_version": 1, + "tables": [interval_table_fixture(source_period=2002)], + } + + +def _interval_preparation_node(document, receipt, intervals, bindings=None): + table = intervals["tables"][0] + bindings = interval_bindings(table) if bindings is None else bindings + params = { + "bindings": canonical_text(bindings), + "references": canonical_text(document), + "references_sha256": reference_document_sha256(document), + "intervals": canonical_text(intervals), + } + if receipt is not None: + params["reference_resolution"] = canonical_text(receipt) + return Node( + "prepare", + "calibration.prepare@1", + population="open", + params=params, + outputs=tuple( + Owned("person", band["indicator"], dtype="bool") for band in table["bands"] + ), + artifact_outputs=(ArtifactOutput("preparation", CALIBRATION_PREPARATION_TYPE),), + ) + + +def _with_interval_inputs(tables, persons): + pattern = [0, -400, 99_999, 100_000, 100_001, 1_000_000, 1_000_001, 7] + statuses = ["included", "included", "included", "excluded"] + tables["person"][INCOME_COLUMN] = [pattern[i % 8] for i in range(persons)] + tables["person"][UNIVERSE_COLUMN] = [statuses[i % 4] for i in range(persons)] + + +def _interval_case(tmp_path): + intervals = _interval_document() + document, effective, receipt = _profile_document(intervals=intervals) + assert effective == intervals + facts = write_facts(tmp_path / "facts.jsonl", toy_facts(CALIBRATION_TARGETS)) + payload, _ = compile_target_surface( + facts, + document, + country=COUNTRY, + references_sha256=reference_document_sha256(document), + reference_resolution=receipt, + ) + frame = toy_frame() + persons = len(frame.table("person")) + context = _context( + _interval_preparation_node(document, receipt, intervals), frame=frame + ) + _with_interval_inputs(context.tables, persons) + prepared = CALIBRATION_PREPARE.run(context) + return intervals, document, receipt, payload, frame, prepared + + +def _interval_problem_context(frame, payload, preparation, prepared_columns): + context = _context( + _problem_node(), + frame=frame, + artifacts={ + "surface": _artifact(payload, TARGET_SURFACE_TYPE), + "preparation": _artifact(preparation, CALIBRATION_PREPARATION_TYPE), + }, + ) + _with_interval_inputs(context.tables, len(frame.table("person"))) + for (entity, name), values in prepared_columns.items(): + context.tables[entity][name] = values.to_numpy(copy=True) + return context + + +def test_interval_preparation_carries_digest_inputs_and_payload(tmp_path): + intervals, _, receipt, _, frame, prepared = _interval_case(tmp_path) + evidence = json.loads(prepared.artifacts["preparation"]) + assert evidence["effective_intervals_sha256"] == reference_document_sha256( + intervals + ) + assert evidence["effective_intervals"] == intervals + (row,) = evidence["interval_inputs"] + persons = frame.table("person") + assert row["table"] == "toy_income" and row["entity"] == "person" + assert row["rows"] == len(persons) + assert row["income_values_sha256"] == ( + reference_intervals_module.interval_input_fingerprint( + [ + [0, -400, 99_999, 100_000, 100_001, 1_000_000, 1_000_001, 7][i % 8] + for i in range(len(persons)) + ] + ) + ) + nil = prepared.columns["person", "toy_income_band_nil"] + b0 = prepared.columns["person", "toy_income_band_b0"] + statuses = [ + ["included", "included", "included", "excluded"][i % 4] + for i in range(len(persons)) + ] + incomes = [ + [0, -400, 99_999, 100_000, 100_001, 1_000_000, 1_000_001, 7][i % 8] + for i in range(len(persons)) + ] + assert nil.tolist() == [ + status == "included" and income <= 0 + for status, income in zip(statuses, incomes, strict=True) + ] + assert b0.tolist() == [ + status == "included" and 0 < income <= 100_000 + for status, income in zip(statuses, incomes, strict=True) + ] + + +def test_problem_binds_boundary_diagnostics_without_changing_targets(tmp_path): + _, document, receipt, payload, frame, prepared = _interval_case(tmp_path) + result = TARGETS_PROBLEM.run( + _interval_problem_context( + frame, payload, prepared.artifacts["preparation"], prepared.columns + ) + ) + decoded = decode_problem(result.artifacts["problem"]) + report = decoded.bindings["interval_diagnostics"] + assert decoded.bindings["interval_diagnostics_sha256"] == reference_document_sha256( + report + ) + assert result.receipt["interval_diagnostics"] == report + assert report["effective_intervals_sha256"] == receipt["effective_intervals_sha256"] + (table,) = report["tables"] + assert table["table"] == "toy_income" + assert table["weight_kind"] == "design" + assert table["publisher_reclassification"] == "unavailable" + evidence = json.loads(prepared.artifacts["preparation"]) + assert ( + table["digests"]["record_order_sha256"] + == (evidence["interval_inputs"][0]["entity_ids_sha256"]) + ) + # The same surface prepared without interval outputs has identical targets. + plain = CALIBRATION_PREPARE.run( + _context( + _preparation_node(document, receipt, _interval_document()), frame=frame + ) + ) + context = _context( + _problem_node(), + frame=frame, + artifacts={ + "surface": _artifact(payload, TARGET_SURFACE_TYPE), + "preparation": _artifact( + plain.artifacts["preparation"], CALIBRATION_PREPARATION_TYPE + ), + }, + ) + context.tables["household"]["prepared_marker"] = 1 + baseline = TARGETS_PROBLEM.run(context) + without = decode_problem(baseline.artifacts["problem"]) + assert "interval_diagnostics" not in without.bindings + np.testing.assert_array_equal( + decoded.problem.target_vector, without.problem.target_vector + ) + np.testing.assert_array_equal( + decoded.problem.target_vector, + [spec.value for spec in decode_target_surface(payload).registry.specs], + ) + + +@pytest.mark.parametrize( + "defect", + [ + "income", + "universe", + "payload", + "dropped_inputs", + "dropped_payload", + "missing_income_column", + ], +) +def test_problem_refuses_stale_interval_preparation(tmp_path, defect): + _, _, _, payload, frame, prepared = _interval_case(tmp_path) + evidence = json.loads(prepared.artifacts["preparation"]) + if defect == "payload": + evidence["effective_intervals"]["tables"][0]["bands"][1]["upper_cent"] = 99_000 + elif defect == "dropped_inputs": + evidence.pop("interval_inputs") + elif defect == "dropped_payload": + evidence.pop("effective_intervals") + context = _interval_problem_context( + frame, payload, canonical_json(evidence), prepared.columns + ) + if defect == "income": + context.tables["person"].loc[0, INCOME_COLUMN] = 100_001 + elif defect == "universe": + context.tables["person"].loc[0, UNIVERSE_COLUMN] = "excluded" + elif defect == "missing_income_column": + context.tables["person"].pop(INCOME_COLUMN) + with pytest.raises(ValueError, match="preparation|missing"): + TARGETS_PROBLEM.run(context) + + +def test_missing_diagnostic_entity_refuses_as_missing_evidence(): + table = reference_intervals_module.interval_table( + interval_table_fixture(entity="tax_unit", source_period=2002) + ) + with pytest.raises(ValueError, match="missing evidence"): + target_kernels_module._interval_diagnostics( + toy_frame(), [table], ENTITIES, "0" * 64 + ) + + +@pytest.mark.parametrize( + "defect", ["wrong_document", "no_receipt", "no_document", "unresolved"] +) +def test_preparation_refuses_wrong_unbound_or_unresolved_intervals(tmp_path, defect): + intervals = _interval_document() + document, _, receipt = _profile_document(intervals=intervals) + supplied = deepcopy(intervals) + if defect == "wrong_document": + supplied["tables"][0]["bands"][1]["upper_cent"] = 99_000 + node = _interval_preparation_node( + document, None if defect == "no_receipt" else receipt, supplied + ) + if defect == "no_document": + document, _, receipt = _profile_document() + node = _interval_preparation_node(document, receipt, intervals) + node = replace( + node, params={k: v for k, v in node.params.items() if k != "intervals"} + ) + frame = toy_frame() + context = _context(node, frame=frame) + _with_interval_inputs(context.tables, len(frame.table("person"))) + if defect == "unresolved": + context.tables["person"].loc[1, UNIVERSE_COLUMN] = "unresolved" + with pytest.raises(ValueError): + CALIBRATION_PREPARE.run(context) + + +@pytest.mark.parametrize( + "kernel", [CALIBRATION_PREPARE, TARGETS_PROBLEM], ids=lambda k: k.ref +) +def test_interval_module_source_changes_preparation_and_problem_identity( + tmp_path, monkeypatch, kernel +): + before = kernel.implementation_hash() + source = Path(reference_intervals_module.__file__) + changed = tmp_path / source.name + changed.write_bytes(source.read_bytes() + b"\n# interval helper mutation\n") + monkeypatch.setattr(reference_intervals_module, "__file__", str(changed)) + assert kernel.implementation_hash() != before diff --git a/packages/microcosm-build/tests/golden/nz_country_spec.json b/packages/microcosm-build/tests/golden/nz_country_spec.json index 056f151afc..e92e5777f4 100644 --- a/packages/microcosm-build/tests/golden/nz_country_spec.json +++ b/packages/microcosm-build/tests/golden/nz_country_spec.json @@ -1,6 +1,6 @@ { "country": "nz", - "fingerprint": "be277bbabfb0c40601a9719aa5f3adeb5b51c10e8adcc9dfad36475b7f35d425", + "fingerprint": "ee205064f341d7c2b51bbe9909b516f76e9b0b67991406337fc1f2adbd863470", "gate_ids": [ "calibration_per_family_fit", "demography_per_family_fit", @@ -45,7 +45,7 @@ "benefit_unit_rule.json": "a9e8cbfd38a64d2f8db0002fe8c31bb5a18fb34dbe59fa943336487ce95e2ca8", "calibration_bindings.json": "2293672e396407a24caaa5788638e7a1e7fc962575bc2551f500fbc309c3fe32", "chronicle_feed.json": "60a80dd0042838b0412baf9dd0985c961b4253a6876d16bcdbb8a2c6c03de0c7", - "country_package.json": "a4f71abaf97cda60c0eedf329dcd3da47fe8167898a94184be2a9a88c55e5a7b", + "country_package.json": "2e8428292b229c6801f813b5b44d7b3a17a0593c8315b381e05959da7f67630d", "currency_bridge.json": "aa6a4b286a6a1a015088f32ac013148553422c3d45eeef7d7a15af08388857ca", "export_contract.json": "2c7414fefe395c4f5822643dcd3bb3544ebb2040259aa007b828a99bd01ae210", "gates.json": "fa08b508ce1c93ef7627ca06f3a9e808f8fa6ea6f23ce24fa54c58241bbb9f7f", @@ -53,8 +53,9 @@ "holdout_references.json": "52b5a2360502156da1392a68350e4e7f61b5fd420fa74428819b65886a9f8655", "precal_references.json": "420361e424405df2312bd21808d6b6f4c3428b6fe90c44c37ab156ce197aa25d", "reference_aging.json": "4987106611ce14b227e483016b384f6c8103e28311c60384df6adbdfba4ca75d", + "reference_intervals.json": "fcc88c0efd5f416aa15a7d5afbeee1d103dd78a136e13b1efc3973d203188412", "reference_inventory.json": "d16ac4e95f5427c7bb1382ff6ae90e93d4b48344d810755fcc1dc6514430244a", - "reference_profiles.json": "323896a354467a4ade5c52c97c2f80acbd62809072e652c801bd209caa667c54", + "reference_profiles.json": "cbdcb3bc9fe9c64f0f9280b40996545a8b20ec3a3d173fbdc1080c683715e6d5", "release_contract.json": "6cd5e3a4a2bf30e4025e04e964c393b24040937d13db7e6a07b3966a55f3b114", "reporting_classifiers.json": "2700a8dab0bcc186a0ce354a09c7cab127e8b07589b15fb7d63c7f1581041ad8", "scenarios.json": "3f1b5e40d283d7a1baf1a6990f2ca08a0ca39151a8cff028a49455f95e82e9c7", @@ -88,6 +89,7 @@ "holdout_references.json", "precal_references.json", "reference_aging.json", + "reference_intervals.json", "reference_inventory.json", "reference_profiles.json", "release_contract.json", diff --git a/test_support/microcosm_build/reference_intervals.py b/test_support/microcosm_build/reference_intervals.py new file mode 100644 index 0000000000..ad7d8bf489 --- /dev/null +++ b/test_support/microcosm_build/reference_intervals.py @@ -0,0 +1,190 @@ +"""Toy interval tables and ageing profiles for exercising interval plumbing.""" + +from __future__ import annotations + +import json +from copy import deepcopy + +from microcosm.build.reference_factors import resolve_reference_factor +from test_support.microcosm_build.reference_factors import factor_fixture +from test_support.microcosm_build.transport_graph import reference_document + +#: Anchor levels against a source level of 100: factors 1.05, 1.1 and 1.15. +PROFILE_ANCHORS = {"low": "105", "central": "110", "high": "115"} +INCOME_COLUMN = "toy_income_cents" +UNIVERSE_COLUMN = "toy_income_universe" + + +def interval_table_fixture( + *, + table_id="toy_income", + entity="person", + cuts=(100_000, 1_000_000), + nil=True, + closure="right", + first=0, + source_period=2001, + target_period=2002, + companions=False, + indicator_prefix="toy_income_band", +): + """One declared table; companions name ``toy_{count,money}_``.""" + bands = [] + if nil: + bands.append( + {"id": "nil", "kind": "nil", "indicator": f"{indicator_prefix}_nil"} + ) + for index, upper in enumerate((*cuts, None)): + bands.append( + { + "id": f"b{index}", + "kind": "positive", + "upper_cent": upper, + "indicator": f"{indicator_prefix}_b{index}", + } + ) + if companions: + for band in bands: + band.update( + count_reference=f"toy_count_{band['id']}", + amount_reference=f"toy_money_{band['id']}", + ) + return { + "id": table_id, + "entity": entity, + "currency": "XTS", + "input_unit": "integer_cent", + "income_column": INCOME_COLUMN, + "universe_column": UNIVERSE_COLUMN, + "source_period": source_period, + "target_period": target_period, + "positive_band_closure": closure, + "nil_band": "present" if nil else "absent", + "negative_income_policy": "floor_zero", + "first_lower_cut_cents": first, + "source_locator": "Toy!A1:A9", + "diagnostics": { + "boundary_window_cents": 100, + "alternative_positive_band_closure": "left" + if closure == "right" + else "right", + }, + "bands": bands, + } + + +def interval_bindings(table, *, entity=None): + """Calibration bindings whose outputs are the table's band indicators.""" + entity = table["entity"] if entity is None else entity + return { + "schema_version": 1, + "required_inputs": {entity: [f"{entity}_id"]}, + "input_domains": {entity: {f"{entity}_id": {"kind": "identity"}}}, + "columns": [ + { + "entity": entity, + "name": band["indicator"], + "interval": {"table": table["id"], "band": band["id"]}, + } + for band in table["bands"] + ], + } + + +def _factor_entry(profile): + declaration, facts, identity = factor_fixture(anchor_level=PROFILE_ANCHORS[profile]) + declaration["id"] = f"toy_{profile}" + factor, receipt = resolve_reference_factor(declaration, facts, identity) + return identity, { + "declaration": declaration, + "operational_factor": factor, + "receipt": receipt, + } + + +def interval_profile_fixture(): + """References, profiles, factor context and intervals for low/central/high. + + Every band has parked count and money companions filtered on its own + indicator; each profile scales the table with its own factor. + """ + table = interval_table_fixture(companions=True) + references = [] + for band in table["bands"]: + for kind in ("count", "money"): + name = f"toy_{kind}_{band['id']}" + references.append( + { + "name": name, + "entity": "person", + "measure": "people" if kind == "count" else "toy_income", + "filter": band["indicator"], + "period": 2002, + "family": "toy", + "category_id": "toy.population", + "period_match_policy": "latest_not_after", + "ledger_selector": { + "aggregate_fact_key": f"toy-{kind}-{band['id']}", + "source_name": "toy_office", + "source_measure_id": kind, + "source_concept": f"toy.{kind}", + "entity_name": "person", + "period_type": "year", + "period_value": 2001, + "dimensions": ["band"], + "dimension_values": {"band": band["id"]}, + }, + "metadata": { + "contract_target_id": name, + "reference_value_kind": kind, + "activation_status": "requires_prerequisites", + "required_reference_profile": "index", + "required_measurement_binding": "reviewed_measurement", + "reference_prerequisites": json.dumps( + [ + "reference_profile:index", + "measurement:reviewed_measurement", + ] + ), + "parking_reason": "Reviewed measurement is required.", + }, + } + ) + identity, factors = None, [] + for profile in PROFILE_ANCHORS: + identity, entry = _factor_entry(profile) + factors.append(entry) + context = {"schema_version": 1, "feed_identity": identity, "factors": factors} + transformation = { + "operation": "scale_money_and_intervals", + "reference_names": [ + row["name"] for row in references if row["name"].startswith("toy_money") + ], + "count_reference_names": [ + row["name"] for row in references if row["name"].startswith("toy_count") + ], + "interval_table_ids": [table["id"]], + "required_measurement_binding": "reviewed_measurement", + } + profiles = { + "schema_version": 1, + "reference_resource": "target_references", + "factor_context_resource": "factor_context", + "interval_resource": "profile_intervals", + "dimensions": { + "index": { + "default": "central", + "profiles": { + profile: { + "transformations": [ + {**deepcopy(transformation), "factor_id": f"toy_{profile}"} + ] + } + for profile in PROFILE_ANCHORS + }, + } + }, + } + base = reference_document(references) + intervals = {"schema_version": 1, "tables": [table]} + return base, profiles, context, intervals diff --git a/tools/generate_nz_target_references.py b/tools/generate_nz_target_references.py index af68d92fc8..38d31b7384 100644 --- a/tools/generate_nz_target_references.py +++ b/tools/generate_nz_target_references.py @@ -12,7 +12,9 @@ import json import math import os +import re from collections import Counter +from decimal import Decimal from pathlib import Path from microcosm.build.ledger_artifact import load_ledger_consumer_artifact @@ -84,6 +86,34 @@ # Reviewed national detail-row map: nil row 8 through open tail row 244. # Published national total row 246 is diagnostic and is deliberately absent. IRD_NATIONAL_ROWS = tuple(f"row_{row}" for row in range(8, 245)) +IRD_AGE_ROWS = tuple(f"row_{row}" for row in range(6, 189)) +IRD_NATIONAL_TABLE = "ird_taxable_income_national_ty2024" +IRD_AGE_TABLE = "ird_taxable_income_age_ty2024" +# Columns name the d1952 stand-in measure, not IRD's taxable-income definition. +IRD_PROXY_INCOME_COLUMN = "ird_taxable_income_proxy_cents" +IRD_PROXY_UNIVERSE_COLUMN = "ird_taxable_income_proxy_universe_status" +# Rows whose published lower label repeats the previous row's upper endpoint. +IRD_SHARED_ENDPOINT_ROWS = { + IRD_NATIONAL_TABLE: ("row_30", "row_60", "row_90", "row_230"), + IRD_AGE_TABLE: ("row_28", "row_58", "row_88"), +} +INTERVAL_DISCLOSURE = ( + "IRD publishes no band closure or cents semantics for these tables. The " + "cuts, closure, nil band and integer-cent arithmetic are Microcosm's declared " + "convention (G8-r open rulings 2, section 1, 23 October 2026), not IRD's own " + "semantics: t = max(0, x) in integer cents with no dollar rounding; t == 0 is " + "the nil band, including qualifying loss records; each positive band is " + "previous_upper < t <= upper; and the open tail is t > its lower cut. Four " + "published label pairs share an endpoint ($20,000, $50,000, $80,000 and " + "$300,000: national rows 29/30, 59/60, 89/90 and 229/230; age rows 27/28, " + "57/58 and 87/88); the ordered right-closed cuts assign each shared endpoint " + "to the lower band. Ageing scales every finite cut exactly by the selected " + "factor and keeps fractional cents. The income and universe columns name the " + "d1952 stand-in measure rather than IRD's definition; no producer exists yet " + "(G8-r2c) and no active row requests them. Boundary diagnostics report " + "weighted mass within $1 of every cut and the fixed-weight change under the " + "left-closed alternative; publisher reclassification is unavailable." +) RECEIPT_REASON = "G8-w: age-stratified receipt assignment is not supported by the current total-only receipt kernel; preserve the source snapshot and age filters." NZS_REASON = "NZ Super receipt binding and prepared receipt flag are not implemented." VP_REASON = "Veterans Pension is unsupported: no veteran facts or producer; a missing flag must refuse, never default to false." @@ -734,12 +764,182 @@ def build_reference_documents(artifact, *, package_dir=SPEC): } documents["calibration_bindings.json"] = calibration_bindings() documents["reference_profiles.json"] = _reference_profiles(inventory, targets) + documents["reference_intervals.json"] = _reference_intervals(inventory) documents["reference_inventory.json"] = _receipts( inventory, erp, documents, feed_pin, ta_facts ) return documents +def _national_upper_dollars(row): + if row == 9: + return 100 + if row == 10: + return 1000 + if 11 <= row <= 209: + return 1000 * (row - 9) + if 210 <= row <= 229: + return 200000 + 5000 * (row - 209) + if 230 <= row <= 243: + return 300000 + 50000 * (row - 229) + if row == 244: + return None + raise ValueError(f"No national taxable-income cut for row {row}") + + +def _age_upper_dollars(row): + if row == 7: + return 100 + if row == 8: + return 1000 + if 9 <= row <= 187: + return 1000 * (row - 7) + if row == 188: + return None + raise ValueError(f"No age-table taxable-income cut for row {row}") + + +def _label_bounds(label): + """Read a published band label as (lower, upper) dollars; upper None is open.""" + if label == "nil": + return None + match = re.fullmatch(r"\$([\d,]+(?:\.\d+)?)\s*-\s*\$([\d,]+)", label) + if match: + lower, upper = match.groups() + return Decimal(lower.replace(",", "")), Decimal(upper.replace(",", "")) + match = re.fullmatch(r"Over \$([\d,]+)", label) + if match: + return Decimal(match.group(1).replace(",", "")), None + raise ValueError(f"Unreadable taxable-income band label {label!r}") + + +def _band_labels(facts, rows): + labels = {} + for fact in facts: + if fact["layout"]["table_record_kind"] != "detail": + continue + row = fact["layout"]["source_row_id"] + label = fact["dimensions"]["taxable_income_band_source_label"] + if labels.setdefault(row, label) != label: + raise ValueError(f"Taxable-income row {row} has inconsistent labels") + if tuple(sorted(labels, key=lambda row: int(row[4:]))) != rows: + raise ValueError("Taxable-income detail rows differ from the reviewed roster") + return labels + + +def _interval_table(table_id, rows, labels, upper_dollars, locator, companions): + """Generate cuts from the row formulas and check them against the labels.""" + bands, previous, shared = [], Decimal(0), [] + for row_id in rows: + row = int(row_id.removeprefix("row_")) + label = labels[row_id] + bounds = _label_bounds(label) + indicator = ( + f"taxable_income_band_{row_id}_indicator" + if companions + else f"taxable_income_age_table_band_{row_id}_indicator" + ) + band = {"id": row_id} + if bounds is None: + if row_id != rows[0]: + raise ValueError("Only the first taxable-income row is nil") + band.update(kind="nil") + else: + upper = upper_dollars(row) + lower, published_upper = bounds + if published_upper != (None if upper is None else Decimal(upper)): + raise ValueError(f"Row {row_id} label {label!r} disagrees with its cut") + if lower == previous: + shared.append(row_id) + elif lower != previous + (Decimal("0.01") if previous == 0 else 1): + raise ValueError(f"Row {row_id} label {label!r} leaves a gap") + band.update( + kind="positive", upper_cent=None if upper is None else upper * 100 + ) + if upper is not None: + previous = Decimal(upper) + band.update(indicator=indicator, source_label=label) + if companions: + band.update( + count_reference=f"ird_taxable_income_people_by_band__{row_id}", + amount_reference=f"ird_taxable_income_total_by_band__{row_id}", + ) + bands.append(band) + # "Over $X" names its own lower cut, so the open tail is not a shared label. + shared = tuple(row for row in shared if row != rows[-1]) + if shared != IRD_SHARED_ENDPOINT_ROWS[table_id]: + raise ValueError(f"{table_id} shared endpoint rows changed: {shared}") + return { + "id": table_id, + "entity": "person", + "currency": "NZD", + "input_unit": "integer_cent", + "income_column": IRD_PROXY_INCOME_COLUMN, + "universe_column": IRD_PROXY_UNIVERSE_COLUMN, + "source_period": 2024, + "target_period": 2027, + "positive_band_closure": "right", + "nil_band": "present", + "negative_income_policy": "floor_zero", + "first_lower_cut_cents": 0, + "source_locator": locator, + "diagnostics": { + "boundary_window_cents": 100, + "alternative_positive_band_closure": "left", + }, + "bands": bands, + } + + +def _reference_intervals(inventory): + national = _band_labels( + inventory.cells( + "ird_taxable_income_distribution.ty2024.income_bands", + measure="individuals", + ), + IRD_NATIONAL_ROWS, + ) + age = _band_labels( + inventory.cells("ird_taxable_income_distribution.ty2024.age_by_income_band"), + IRD_AGE_ROWS, + ) + tables = [ + _interval_table( + IRD_NATIONAL_TABLE, + IRD_NATIONAL_ROWS, + national, + _national_upper_dollars, + "Income tables 2001-24!BE8:BE244", + companions=True, + ), + _interval_table( + IRD_AGE_TABLE, + IRD_AGE_ROWS, + age, + _age_upper_dollars, + "A6:A188", + companions=False, + ), + ] + tables[0]["description"] = ( + "National TY2024 taxable-income bands, rows 8-244: one nil band, 235 " + "finite bands and the open tail over $1,000,000. Every band names its " + "parked count and amount references." + ) + tables[1]["description"] = ( + "Age-by-income TY2024 taxable-income bands, rows 6-188: one nil band, " + "181 finite bands and the open tail over $180,000. Declared and " + "validated only; no reference companions and no fit rows." + ) + return { + "schema_version": 1, + "country": "nz", + "description": "Generated by tools/generate_nz_target_references.py from the row formulas and checked against the pinned feed's own band labels.", + "disclosure": INTERVAL_DISCLOSURE, + "tables": tables, + } + + def _allocation_operands(inventory, row, recipients): selector = row["ledger_selector"] region = selector["geography_id"] @@ -813,6 +1013,7 @@ def _reference_profiles(inventory, rows): return { "schema_version": 1, "factor_context_resource": "reference_aging.json", + "interval_resource": "reference_intervals.json", "dimensions": { "census_residual_allocation": {"default": "mar", "profiles": profiles}, "aging_index": { @@ -831,7 +1032,7 @@ def _reference_profiles(inventory, rows): f"ird_taxable_income_people_by_band__{row_id}" for row_id in IRD_NATIONAL_ROWS ], - "interval_table_ids": [], + "interval_table_ids": [IRD_NATIONAL_TABLE], "required_measurement_binding": IRD_MEASUREMENT_BINDING, } ] From f7000820b526b622434ad97054aa9df9c09d718d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 24 Oct 2026 22:19:36 -0400 Subject: [PATCH 07/24] Accept left-closed nil tables with a zero first cut Address the r2b review (GPT-6.1 Sol, 75c27688). - Remove the validator guard that refused positive_band_closure "left" with a nil band and a zero first lower cut. Zero is nil under either closure (_position checks nil before any cut), so the first positive band is 0 < z < upper and nothing belongs to two bands. The refusal forced a one-cent stand-in cut, which moves membership after scaling: at factor two, one cent fell below the first cut. - Replace the refusal test with acceptance tests: classification at the unscaled and twice-scaled cuts, and boundary diagnostics with zero in nil under the declared and alternative closures. Drop the two first=1 workarounds, and let the Hypothesis table generator draw the combination. - Give the composed-graph interval config's Owned output its required dtype; without it the whole test file failed collection. - Correct the changelog's refusal sentence. Verified: test_reference_intervals 107, test_nz_reference_intervals 14, test_transport_target_kernels 67 and test_transport_composed_graph 166 passed. Restoring the guard, and checking nil after the cuts, each turn the three new tests red with the control green. All 12 r2b mutants were killed with the dtype fix in place; left_closed_cut and default_closure, whose test file this changes, were rerun after it and still kill. ruff, format and git diff --check are clean. Co-Authored-By: Claude Opus 5.5 --- changelog.d/nz-reference-intervals.added.md | 4 +- .../microcosm/build/reference_intervals.py | 5 -- .../shared/test_reference_intervals.py | 64 ++++++++++++++++--- .../shared/test_transport_composed_graph.py | 2 +- 4 files changed, 57 insertions(+), 18 deletions(-) diff --git a/changelog.d/nz-reference-intervals.added.md b/changelog.d/nz-reference-intervals.added.md index e1b822534c..95e52379ed 100644 --- a/changelog.d/nz-reference-intervals.added.md +++ b/changelog.d/nz-reference-intervals.added.md @@ -16,8 +16,8 @@ each variant its own effective interval companion. Declare the NZ national (237 bands) and age (183 bands) taxable-income tables under Microcosm's declared convention; the 474 IRD references stay parked. -Require the first cut explicitly and refuse overlapping nil and left-closed -zero bands. Report below-first-cut mass and floored negative records, including +Require the first cut explicitly. Zero is nil under either closure, so with a +nil band and a zero first cut the first positive band starts above zero. Report below-first-cut mass and floored negative records, including weighted counts and signed pre-floor amounts by destination. These diagnostics describe fixed weights before fitting; reporting later weight changes on unfitted records remains G8-r2c/G8-x work. diff --git a/packages/microcosm-build/src/microcosm/build/reference_intervals.py b/packages/microcosm-build/src/microcosm/build/reference_intervals.py index 61652b5689..250be3fd64 100644 --- a/packages/microcosm-build/src/microcosm/build/reference_intervals.py +++ b/packages/microcosm-build/src/microcosm/build/reference_intervals.py @@ -338,11 +338,6 @@ def interval_table(value: object) -> IntervalTable: ) if first < 0: raise ValueError("The first lower cut must be nonnegative.") - if nil_present and closure == "left" and first == 0: - raise ValueError( - "A left-closed table with a nil band requires a positive first lower " - "cut; zero cannot belong to both nil and a positive band." - ) band_ids, indicators, cuts, positive = [], [], [first], [] nil_index, open_tail = None, False companions = set() diff --git a/packages/microcosm-build/tests/engine_free/shared/test_reference_intervals.py b/packages/microcosm-build/tests/engine_free/shared/test_reference_intervals.py index e9743590be..71a02b046c 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_reference_intervals.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_reference_intervals.py @@ -139,7 +139,7 @@ def _classify(table, amounts, universe=None, **kwargs): ], ) def test_each_exact_cut_and_one_cent_either_side(closure, expected): - table = _table(closure=closure, first=1 if closure == "left" else 0) + table = _table(closure=closure) amounts = [-1, 0, 1, 9_999, 10_000, 10_001, 99_999, 100_000, 100_001] assert _classify(table, amounts) == expected @@ -204,13 +204,57 @@ def test_nil_and_loss_records_follow_the_declared_negative_policy(): @pytest.mark.parametrize("scaled", [False, True]) -def test_left_closed_nil_table_refuses_a_zero_first_cut(scaled): - table = _scale(_table(first=1)) if scaled else _table() - table["positive_band_closure"] = "left" - table["diagnostics"]["alternative_positive_band_closure"] = "right" - table["first_lower_cut_cents"] = {"numerator": 0, "denominator": 1} if scaled else 0 - with pytest.raises(ValueError, match="left-closed table with a nil band"): - interval_table(table) +def test_left_closed_nil_table_with_a_zero_first_cut_puts_zero_in_nil_only(scaled): + # Zero is nil under either closure, so the first positive band is + # 0 < z < upper. Scaling keeps the zero cut: one cent stays in b0 rather + # than falling below a nonzero stand-in cut. + table = _table((100, 1_000), closure="left") + amounts = [-1, 0, 1, 99, 100, 999, 1_000, 1_001] + if scaled: + table = _scale(table, {"numerator": 2, "denominator": 1}) + assert table["first_lower_cut_cents"] == {"numerator": 0, "denominator": 1} + amounts = [-1, 0, 1, 199, 200, 1_999, 2_000, 2_001] + assert _classify(table, amounts) == [ + "nil", + "nil", + "b0", + "b0", + "b1", + "b1", + "b2", + "b2", + ] + + +def test_left_closed_nil_zero_first_cut_diagnostics_keep_zero_in_nil(): + table = interval_table(_table((100, 1_000), closure="left")) + report = boundary_diagnostics( + table, + [0, 1, 2, 3], + [-5, 0, 1, 100], + ["included"] * 4, + [2.0, 3.0, 5.0, 7.0], + weight_record_ids=[0, 1, 2, 3], + weight_kind="design", + ) + bands = {band["band"]: band for band in report["bands"]} + assert bands["nil"]["declared"]["records"] == 2 + assert Fraction(**bands["nil"]["declared"]["weighted_count"]) == 5 + assert "alternative" not in bands["nil"] + # Declared left: 1 is b0 and 100 is b1. The right-closed alternative puts + # both in b0; zero stays nil and nothing is below the first cut. + assert [bands[band]["declared"]["records"] for band in ("b0", "b1", "b2")] == [ + 1, + 1, + 0, + ] + assert [bands[band]["alternative"]["records"] for band in ("b0", "b1", "b2")] == [ + 2, + 0, + 0, + ] + assert report["below_first_cut"]["declared"]["records"] == 0 + assert report["below_first_cut"]["alternative"]["records"] == 0 def test_left_closed_nil_table_keeps_a_positive_fractional_first_cut_after_scaling(): @@ -454,7 +498,7 @@ def test_invalid_declared_semantics_refuse(field, value): def test_the_diagnostic_alternative_is_the_other_closure(): - table = _table(closure="left", first=1) + table = _table(closure="left") assert table["diagnostics"]["alternative_positive_band_closure"] == "right" interval_table(table) table["diagnostics"]["alternative_positive_band_closure"] = "left" @@ -841,7 +885,7 @@ def _tables(draw, big=False): step = st.integers(min_value=1, max_value=10**30 if big else 50_000) closure = draw(st.sampled_from(["left", "right"])) nil = draw(st.booleans()) - first = draw(step if nil and closure == "left" else st.one_of(st.just(0), step)) + first = draw(st.one_of(st.just(0), step)) cuts, previous = [], first for _ in range(count): previous += draw(step) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_transport_composed_graph.py b/packages/microcosm-build/tests/engine_free/shared/test_transport_composed_graph.py index 469d9352b0..a380a3303e 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_transport_composed_graph.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_transport_composed_graph.py @@ -1148,7 +1148,7 @@ def _interval_prepare_spec(): _INTERVAL_CONFIG = TransportGraphConfig( - engine_refs={}, create_outputs=(Owned("person", "age"),) + engine_refs={}, create_outputs=(Owned("person", "age", "int64"),) ) From 9b5a582908f3416df389d03aa019a4be3492f9c9 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 24 Oct 2026 18:48:54 -0400 Subject: [PATCH 08/24] Normalize donor money with exact declared factors Add a pure money_normalization module and a money.normalize@1 transport kernel. A declaration names a cited decimal-text or feed-replayed FX rate, its quote direction, half-even rounding, the donor money period, and per-family factors or reasoned identities. Both rounding stages and the receipt sums use exact fractions; non-finite inputs refuse and every integer companion is checked against int64 bounds. The kernel replays its factors against the authenticated feed and refuses a donor period that differs from the one CREATE records. CREATE records the donor H5's _time_period as donor_time_period in its metadata and receipt in every country. DonorBank reads it and refuses a value that is not a one-row integer series. Producer checks are opt-in. With a money_normalization resource, activation names every consumed money column without exactly one producer, and compose refuses a second normalization along structural population inheritance after each graph pass of the ancestry guard. A test checks that, for a spec without the resource, composition and activation are byte-identical with the hooks removed. reference_factors gains the additive money_normalization_input purpose. NZ declares generated calendar-2024 donor factors with the existing factors' validity and explicit per-profile selection. No NZ graph node reads the declaration yet (G8-w3). Ageing non-wage money by AWE growth is disclosed as provisional. Co-Authored-By: GPT-6.1 Sol Co-Authored-By: Claude Opus 5.5 --- changelog.d/nz-money-normalization.added.md | 20 + .../microcosm/build/money_normalization.py | 743 ++++ .../microcosm/build/nz/country_package.json | 5 + .../microcosm/build/nz/currency_bridge.json | 28 +- .../build/nz/money_normalization.json | 125 + .../microcosm/build/nz/reference_aging.json | 3606 +++++++++++++++++ .../src/microcosm/build/reference_factors.py | 31 +- .../src/microcosm/build/transport/compose.py | 294 +- .../build/transport/money_kernels.py | 295 ++ .../build/transport/population_kernels.py | 2 + .../src/microcosm/build/transport/registry.py | 4 +- .../shared/test_ledger_artifact.py | 1 + .../shared/test_money_normalization.py | 791 ++++ .../shared/test_nz_money_normalization.py | 240 ++ .../shared/test_nz_reference_aging.py | 71 +- .../shared/test_nz_spec_package.py | 19 +- .../shared/test_reference_factors.py | 17 + .../shared/test_reference_profiles.py | 30 + .../test_spec_engine_country_bundles.py | 1 + .../shared/test_transport_money_kernels.py | 804 ++++ .../test_transport_population_kernels.py | 23 +- .../tests/golden/nz_country_spec.json | 10 +- .../src/microcosm/frame/transport.py | 22 +- .../shared/test_transport_donor.py | 34 + .../microcosm_build/money_normalization.py | 592 +++ .../microcosm_build/transport_population.py | 7 +- tools/generate_nz_reference_aging.py | 47 +- 27 files changed, 7820 insertions(+), 42 deletions(-) create mode 100644 changelog.d/nz-money-normalization.added.md create mode 100644 packages/microcosm-build/src/microcosm/build/money_normalization.py create mode 100644 packages/microcosm-build/src/microcosm/build/nz/money_normalization.json create mode 100644 packages/microcosm-build/src/microcosm/build/transport/money_kernels.py create mode 100644 packages/microcosm-build/tests/engine_free/shared/test_money_normalization.py create mode 100644 packages/microcosm-build/tests/engine_free/shared/test_nz_money_normalization.py create mode 100644 packages/microcosm-build/tests/engine_free/shared/test_transport_money_kernels.py create mode 100644 test_support/microcosm_build/money_normalization.py diff --git a/changelog.d/nz-money-normalization.added.md b/changelog.d/nz-money-normalization.added.md new file mode 100644 index 0000000000..2422d3bd94 --- /dev/null +++ b/changelog.d/nz-money-normalization.added.md @@ -0,0 +1,20 @@ +Add exact, declared donor money normalization: a pure module that validates a +complete money-normalization declaration (cited decimal-text or feed-replayed +rate, explicit quote, half-even rounding, donor money period, per-family factors +or explicit identities, and quantile-mapped or pending rosters) and applies it +with exact rationals, and a `money.normalize@1` transport kernel that replays +its factors against the authenticated feed, refuses a donor money period that +differs from the one CREATE records, and adds an integer cents companion per +column. CREATE now records the donor's `_time_period`. An opt-in preflight names +every consumed money column without exactly one producer and refuses a second +normalization along structural population inheritance, including a currency +conversion followed by normalization; cross-population evidence edges do not +change money ownership. Specs without the resource are unchanged. Factors gain +the additive `money_normalization_input` purpose. Declare New Zealand's +generated calendar-2024 donor factors with the existing factors' validity and +explicit per-profile selection. The donor period is a declared convention +covering flows and asset stocks, checked for consistency against `_time_period`; +ageing non-wage money by AWE growth is provisional until non-wage ageing is +ruled on. The normalization resource is the single source of the USD to NZD +rate. Both monetary rounding stages and receipt sums use exact fractions, reject +non-finite inputs, and check every integer companion for overflow. diff --git a/packages/microcosm-build/src/microcosm/build/money_normalization.py b/packages/microcosm-build/src/microcosm/build/money_normalization.py new file mode 100644 index 0000000000..7828e59a0f --- /dev/null +++ b/packages/microcosm-build/src/microcosm/build/money_normalization.py @@ -0,0 +1,743 @@ +"""Exact, declared normalization of donor money columns. + +A country declares, as spec data, how each donor money concept reaches its +destination currency and money period: one exchange rate and one feed-derived +period factor per index family, or a named quantile map that is still owed. +This module validates that declaration and applies it with exact rationals. +It holds no country, currency, rate, factor, series or date. + +For a binary64 donor value ``v``: + +- ``x`` is the shortest round-trip decimal of ``v`` (``repr``), read exactly. +- The first rounding converts currency. With the rate as the exact rational + ``num/den`` of its declared decimal, ``to_per_from`` gives + ``m1 = half_even(x * num/den * 10**digits)`` and ``from_per_to`` gives + ``m1 = half_even(x * 10**digits * den/num)``; a quoted rate is never + inverted through a rounded constant. +- The second rounding applies the selected factor ``p / 10**k`` from its + authenticated receipt: ``m2 = half_even(m1 * p / 10**k)``. +- The rewritten column is ``m2 / 10**digits`` and the integer companion + ```` is ``m2``. + +Rounding twice (currency, then factor) can differ from rounding +``x * rate * factor`` once. Each rounding moves a value by at most half a +minor unit, so ``|m2 - half_even(exact * f)| <= floor(1 + f / 2)`` minor +units, which is one minor unit for every factor below 2. + +Every normalized column maps through its concept's index family to one +declared factor, or to an explicit identity with a reason; nothing falls back +to a factor of one. Every money concept that survives transport is declared +exactly once: normalized, quantile mapped or pending. +""" + +from __future__ import annotations + +import math +import re +from collections.abc import Iterable, Mapping +from datetime import date +from decimal import Decimal, InvalidOperation +from fractions import Fraction + +import numpy as np +import pandas as pd + +from microcosm.build.ledger_artifact import chronicle_feed_identity +from microcosm.build.ledger_targets import ( + ALLOWED_ASSERTION_POLICIES, + selector_field_is_supported, +) +from microcosm.build.reference_factors import ( + factor_document_sha256, + resolve_feed_operand, +) +from microcosm.frame.concepts import ( + CONCEPTS, + IndexFamily, + TransportRule, + Unit, + concept_for_column, +) + +__all__ = [ + "MONEY_FACTOR_PURPOSE", + "check_money_factor", + "money_column_rosters", + "money_concept_columns", + "money_factor_inputs", + "normalize_money_tables", + "normalized_column_families", + "resolve_money_rate", + "selected_money_factors", + "validate_money_normalization", +] + +#: The only factor purpose a money normalization may apply. +MONEY_FACTOR_PURPOSE = "money_normalization_input" +_QUOTES = frozenset({"to_per_from", "from_per_to"}) +_ROUNDING_MODES = frozenset({"half_even"}) +_PERIOD_TYPES = frozenset({"calendar_year"}) +_PERIOD_BASES = frozenset({"declared_convention", "authenticated"}) +_RATE_FORMS = frozenset({"decimal_text", "feed_selector"}) +_MAX_MONEY_DIGITS = 6 +_INT64_MIN = -(2**63) +_INT64_MAX = 2**63 - 1 +_FIELDS = frozenset( + { + "schema_version", + "from_currency", + "to_currency", + "rate", + "quote", + "rounding", + "donor_money_period", + "columns", + "index_family_factors", + "factor_profile", + "cents_suffix", + "quantile_mapped", + "pending", + } +) +_DESCRIPTIVE_FIELDS = frozenset({"description", "status", "notes"}) +_CURRENCY = re.compile(r"[A-Z]{3}") +_DECIMAL_TEXT = re.compile(r"[0-9]+(?:\.[0-9]+)?") +_SUFFIX = re.compile(r"_[a-z0-9_]+") +_ROSTERS = ("normalized", "quantile_mapped", "pending") + + +def _mapping(value: object, label: str) -> dict: + if not isinstance(value, Mapping): + raise ValueError(f"Money normalization {label} must be an object.") + return dict(value) + + +def _closed(value: object, fields: frozenset, label: str) -> dict: + row = _mapping(value, label) + unknown = sorted(row.keys() - fields) + if unknown: + raise ValueError(f"Money normalization {label} has unknown fields {unknown}.") + missing = sorted(fields - row.keys()) + if missing: + raise ValueError(f"Money normalization {label} is missing fields {missing}.") + return row + + +def _text(value: object, label: str) -> str: + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"Money normalization {label} must be nonempty text.") + return value + + +def _decimal_text(value: object, label: str) -> Fraction: + """A positive decimal written as JSON text; a JSON number is refused.""" + if not isinstance(value, str): + raise ValueError( + f"Money normalization {label} must be decimal text, never a JSON " + "number: a binary float cannot carry the declared decimal exactly." + ) + if _DECIMAL_TEXT.fullmatch(value) is None: + raise ValueError(f"Money normalization {label} must be plain decimal text.") + try: + result = Fraction(Decimal(value)) + except (InvalidOperation, ValueError) as error: + raise ValueError(f"Money normalization {label} is not decimal text.") from error + if result <= 0: + raise ValueError(f"Money normalization {label} must be positive.") + return result + + +def _rate(value: object) -> dict: + row = _mapping(value, "rate") + form = row.get("form") + if form not in _RATE_FORMS: + raise ValueError( + f"Money normalization rate form must be one of {sorted(_RATE_FORMS)}." + ) + if form == "decimal_text": + row = _closed(row, frozenset({"form", "text", "citation"}), "rate") + _decimal_text(row["text"], "rate text") + citation = _mapping(row["citation"], "rate citation") + if not {"publisher", "title", "url"} <= citation.keys() or any( + not isinstance(item, str) or not item for item in citation.values() + ): + raise ValueError( + "Money normalization rate citation needs publisher, title and url " + "as nonempty text." + ) + return row + row = _closed( + row, + frozenset( + { + "form", + "selector", + "validation", + "assertion_policy", + "feed_identity", + "expected_decimal", + } + ), + "rate", + ) + selector = _mapping(row["selector"], "rate selector") + if not selector or any(not selector_field_is_supported(key) for key in selector): + raise ValueError( + "Money normalization rate selector must use the closed selector vocabulary." + ) + _mapping(row["validation"], "rate validation") + if row["assertion_policy"] not in ALLOWED_ASSERTION_POLICIES: + raise ValueError("Unknown money normalization rate assertion policy.") + row["feed_identity"] = chronicle_feed_identity( + _mapping(row["feed_identity"], "rate feed identity") + ) + _decimal_text(row["expected_decimal"], "rate expected_decimal") + return row + + +def _period(value: object) -> dict: + row = _closed( + value, + frozenset({"type", "value", "basis", "evidence"}), + "donor_money_period", + ) + if row["type"] not in _PERIOD_TYPES: + raise ValueError( + f"Money normalization donor period type must be one of {sorted(_PERIOD_TYPES)}." + ) + if type(row["value"]) is not int or not 1 <= row["value"] <= 9999: + raise ValueError("Money normalization donor period value must be a year.") + if row["basis"] not in _PERIOD_BASES: + raise ValueError( + "Money normalization donor period basis must be one of " + f"{sorted(_PERIOD_BASES)}." + ) + evidence = row["evidence"] + if not isinstance(evidence, list) or not evidence: + raise ValueError( + "Money normalization donor period evidence must cite its basis." + ) + for citation in evidence: + _text(citation, "donor period evidence") + return row + + +def _period_coverage(period: Mapping) -> tuple[str, str]: + year = period["value"] + return date(year, 1, 1).isoformat(), date(year, 12, 31).isoformat() + + +def _money_concept(entity: object, column: object, label: str): + if not isinstance(entity, str) or not isinstance(column, str) or not column: + raise ValueError(f"Money normalization {label} must name entity columns.") + item = concept_for_column(entity, column) + if item is None or item.unit is not Unit.BASE_CURRENCY: + raise ValueError( + f"Money normalization {label} {entity}.{column} is not a money concept." + ) + return item + + +def _columns(value: object) -> dict[str, list[str]]: + columns = _mapping(value, "columns") + if not columns: + raise ValueError("Money normalization columns must name normalized columns.") + result = {} + for entity, names in columns.items(): + if ( + not isinstance(names, list) + or not names + or len(set(map(str, names))) != len(names) + ): + raise ValueError( + "Money normalization columns must be nonempty lists of distinct names." + ) + for name in names: + _money_concept(entity, name, "column") + result[entity] = list(names) + return result + + +def _roster(value: object, label: str) -> dict[str, dict[str, dict]]: + roster = _mapping(value, label) + result = {} + for entity, rows in roster.items(): + rows = _mapping(rows, f"{label} entity") + if not rows: + raise ValueError(f"Money normalization {label} lists an empty entity.") + result[entity] = {} + for column, row in rows.items(): + _money_concept(entity, column, label) + row = _closed(row, frozenset({"awaits", "reason"}), f"{label} entry") + awaits = row["awaits"] + if ( + not isinstance(awaits, list) + or not awaits + or any(not isinstance(item, str) or not item for item in awaits) + or len(set(awaits)) != len(awaits) + ): + raise ValueError( + f"Money normalization {label} entry must name the references " + "it awaits." + ) + _text(row["reason"], f"{label} reason") + result[entity][column] = row + return result + + +def _profile(value: object) -> dict | None: + if value is None: + return None + row = _closed( + value, frozenset({"dimension", "profiles", "central_profile"}), "factor_profile" + ) + _text(row["dimension"], "factor profile dimension") + profiles = row["profiles"] + if ( + not isinstance(profiles, list) + or not profiles + or any(not isinstance(name, str) or not name for name in profiles) + or len(set(profiles)) != len(profiles) + ): + raise ValueError("Money normalization factor profiles must be distinct names.") + if row["central_profile"] not in profiles: + raise ValueError("Money normalization central profile must be a profile.") + return row + + +def _family_entry(value: object, family: str, profile: dict | None) -> dict: + row = _mapping(value, f"index family {family!r}") + if "identity" in row: + row = _closed(row, frozenset({"identity", "reason"}), f"identity {family!r}") + if row["identity"] is not True: + raise ValueError( + f"Money normalization identity family {family!r} must declare " + "identity true." + ) + _text(row["reason"], f"identity reason for {family!r}") + return row + if profile is None: + row = _closed( + row, frozenset({"factor_id", "disclosure"}), f"index family {family!r}" + ) + _text(row["factor_id"], f"factor id for {family!r}") + else: + row = _closed( + row, frozenset({"factors", "disclosure"}), f"index family {family!r}" + ) + factors = _mapping(row["factors"], f"factors for {family!r}") + if set(factors) != set(profile["profiles"]): + raise ValueError( + f"Money normalization family {family!r} must name one factor for " + "every declared profile." + ) + for name in factors.values(): + _text(name, f"factor id for {family!r}") + _text(row["disclosure"], f"disclosure for {family!r}") + return row + + +def money_concept_columns() -> dict[tuple[str, str], object]: + """Every money concept column, keyed ``(entity, column)``.""" + return { + (item.entity, item.name): item + for item in CONCEPTS + if item.unit is Unit.BASE_CURRENCY + } + + +def validate_money_normalization(document: object) -> dict: + """Validate a complete declaration; every field is required. + + Descriptive ``description``, ``status`` and ``notes`` text may ride along + and is returned unchanged; nothing reads it. + """ + row = _mapping(document, "declaration") + descriptive = {key: row.pop(key) for key in _DESCRIPTIVE_FIELDS if key in row} + for key, value in descriptive.items(): + _text(value, key) + row = _closed(row, _FIELDS, "declaration") + if type(row["schema_version"]) is not int or row["schema_version"] != 1: + raise ValueError("Money normalization requires schema_version 1.") + for key in ("from_currency", "to_currency"): + if not isinstance(row[key], str) or _CURRENCY.fullmatch(row[key]) is None: + raise ValueError(f"Money normalization {key} must be an ISO 4217 code.") + if row["from_currency"] == row["to_currency"]: + raise ValueError("Money normalization converts between distinct currencies.") + row["rate"] = _rate(row["rate"]) + if row["quote"] not in _QUOTES: + raise ValueError(f"Money normalization quote must be one of {sorted(_QUOTES)}.") + rounding = _closed(row["rounding"], frozenset({"digits", "mode"}), "rounding") + if ( + type(rounding["digits"]) is not int + or not 0 <= rounding["digits"] <= _MAX_MONEY_DIGITS + ): + raise ValueError( + f"Money normalization rounding digits must be 0 to {_MAX_MONEY_DIGITS}." + ) + if rounding["mode"] not in _ROUNDING_MODES: + raise ValueError("Money normalization rounding mode must be half_even.") + row["donor_money_period"] = _period(row["donor_money_period"]) + row["columns"] = _columns(row["columns"]) + if ( + not isinstance(row["cents_suffix"], str) + or _SUFFIX.fullmatch(row["cents_suffix"]) is None + ): + raise ValueError( + "Money normalization cents_suffix must be lowercase text starting with _." + ) + profile = _profile(row["factor_profile"]) + row["quantile_mapped"] = _roster(row["quantile_mapped"], "quantile_mapped") + row["pending"] = _roster(row["pending"], "pending") + seen: dict[tuple[str, str], str] = {} + for roster, entity, column in ( + *( + ("normalized", entity, column) + for entity, names in row["columns"].items() + for column in names + ), + *( + (name, entity, column) + for name in ("quantile_mapped", "pending") + for entity, rows in row[name].items() + for column in rows + ), + ): + key = (entity, column) + if key in seen: + raise ValueError( + f"Money column {entity}.{column} is declared both {seen[key]} and " + f"{roster}; each column has exactly one producer route." + ) + seen[key] = roster + companion = f"{column}{row['cents_suffix']}" + if concept_for_column(entity, companion) is not None: + raise ValueError( + f"Money normalization companion {entity}.{companion} collides with " + "a concept column." + ) + unclassified = sorted( + f"{entity}.{column}" + for (entity, column), item in money_concept_columns().items() + if item.transport is not TransportRule.DROP and (entity, column) not in seen + ) + if unclassified: + raise ValueError( + "Money normalization must declare every transported money concept as " + f"normalized, quantile_mapped or pending; unclassified: {unclassified}." + ) + families = _mapping(row["index_family_factors"], "index_family_factors") + used = { + money_concept_columns()[entity, column].monetary.index_family.value + for entity, names in row["columns"].items() + for column in names + } + known = {item.value for item in IndexFamily} + if set(families) - known: + raise ValueError( + f"Money normalization names unknown index families {sorted(set(families) - known)}." + ) + unmapped = sorted(used - set(families)) + if unmapped: + raise ValueError( + f"Money normalization index families {unmapped} have no declared factor " + "or explicit identity; a family is never silently left at one." + ) + unused = sorted(set(families) - used) + if unused: + raise ValueError( + f"Money normalization declares index families {unused} that no " + "normalized column uses." + ) + row["index_family_factors"] = { + family: _family_entry(entry, family, profile) + for family, entry in families.items() + } + row["factor_profile"] = profile + return {**row, **descriptive} + + +def money_column_rosters(declaration: Mapping) -> dict[tuple[str, str], tuple]: + """``(entity, column)`` to ``(route, awaits)`` for every declared column.""" + result = { + (entity, column): ("normalized", ()) + for entity, names in declaration["columns"].items() + for column in names + } + for route in ("quantile_mapped", "pending"): + for entity, rows in declaration[route].items(): + for column, row in rows.items(): + result[entity, column] = (route, tuple(row["awaits"])) + return result + + +def normalized_column_families(declaration: Mapping) -> dict[tuple[str, str], str]: + """Each normalized column's index family, read from the concept schema.""" + concepts = money_concept_columns() + return { + (entity, column): concepts[entity, column].monetary.index_family.value + for entity, names in declaration["columns"].items() + for column in names + } + + +def selected_money_factors( + declaration: Mapping, profile_selection: Mapping +) -> tuple[str | None, dict[str, str | None]]: + """The selected profile and each family's factor id (``None``: identity). + + A selection that omits the declared dimension selects its central profile, + as a scenario that leaves a knob at its central value does. + """ + selection = _mapping(profile_selection, "profile selection") + profile = declaration["factor_profile"] + chosen = None + if profile is not None: + chosen = selection.get(profile["dimension"], profile["central_profile"]) + if chosen not in profile["profiles"]: + raise ValueError( + f"Unknown money normalization profile {chosen!r} for " + f"{profile['dimension']!r}." + ) + factors = {} + for family, entry in declaration["index_family_factors"].items(): + if entry.get("identity") is True: + factors[family] = None + elif profile is None: + factors[family] = entry["factor_id"] + else: + factors[family] = entry["factors"][chosen] + return chosen, factors + + +def check_money_factor(entry: Mapping, declaration: Mapping) -> tuple[int, int]: + """Return ``(p, k)`` for a factor ``p / 10**k`` fit for this declaration. + + The factor must carry the money-normalization purpose, and its source + schedule must cover exactly the declared donor money period. + """ + factor_declaration = entry["declaration"] + if factor_declaration["purpose"] != MONEY_FACTOR_PURPOSE: + raise ValueError( + f"Factor {factor_declaration['id']!r} has purpose " + f"{factor_declaration['purpose']!r}; money normalization applies only " + f"{MONEY_FACTOR_PURPOSE} factors." + ) + schedule = factor_declaration["source_schedule"] + coverage = ( + schedule[0]["validation"]["period_coverage"]["start_date"], + schedule[-1]["validation"]["period_coverage"]["end_date"], + ) + if coverage != _period_coverage(declaration["donor_money_period"]): + raise ValueError( + f"Factor {factor_declaration['id']!r} source schedule covers " + f"{coverage[0]}..{coverage[1]}, not the declared donor money period." + ) + digits = entry["receipt"]["rounding"]["digits"] + rational = entry["operational_factor"] + if ( + type(digits) is not int + or rational["denominator"] != 10**digits + or type(rational["numerator"]) is not int + or rational["numerator"] <= 0 + ): + raise ValueError( + "A money factor must be a positive integer over 10**digits from its receipt." + ) + return rational["numerator"], digits + + +def resolve_money_rate( + declaration: Mapping, + facts: Iterable | None = None, + feed_identity: Mapping | None = None, +) -> tuple[Fraction, dict]: + """The exact rate and its evidence; a feed rate is replayed from facts.""" + rate = declaration["rate"] + if rate["form"] == "decimal_text": + return _decimal_text(rate["text"], "rate text"), { + "form": "decimal_text", + "text": rate["text"], + "citation": dict(rate["citation"]), + } + if facts is None or feed_identity is None: + raise ValueError("A feed-selected rate needs the authenticated feed facts.") + if rate["feed_identity"] != chronicle_feed_identity(feed_identity): + raise ValueError( + "Money rate feed identity mismatch; re-declaration is required." + ) + value, evidence = resolve_feed_operand( + {"selector": rate["selector"], "validation": rate["validation"]}, + facts, + assertion_policy=rate["assertion_policy"], + ) + if value != _decimal_text(rate["expected_decimal"], "rate expected_decimal"): + raise ValueError( + "Money rate replay differs from its declared expected decimal; " + "re-declaration is required." + ) + return value, { + "form": "feed_selector", + "text": rate["expected_decimal"], + "operand": evidence, + } + + +def _half_even(value: Fraction) -> int: + """Round an exact rational to an integer, ties to even.""" + return round(value) + + +def _shortest_decimal(value: object) -> Fraction: + """The shortest round-trip decimal of a float, read without arithmetic.""" + number = float(value) + if not math.isfinite(number): + raise ValueError("Money normalization refuses NaN and infinite amounts.") + return Fraction(Decimal(repr(number))) + + +def _rational(value: Fraction) -> dict: + return {"numerator": value.numerator, "denominator": value.denominator} + + +def _exact_decimal(value: Fraction) -> str: + """Exact decimal text of a rational whose denominator divides 10**k.""" + denominator, places = value.denominator, 0 + while denominator % 10 == 0: + denominator //= 10 + places += 1 + while denominator % 2 == 0 or denominator % 5 == 0: + factor = 2 if denominator % 2 == 0 else 5 + denominator //= factor + places += 1 + if denominator != 1: + raise ValueError("A money sum is always a terminating decimal.") + scaled = int(value * Fraction(10**places)) + sign = "-" if scaled < 0 else "" + digits = str(abs(scaled)).rjust(places + 1, "0") + return sign + (digits if not places else f"{digits[:-places]}.{digits[-places:]}") + + +def _minor_units(values: Iterable[int], digits: int) -> str: + total = sum(values) + sign = "-" if total < 0 else "" + text = str(abs(total)).rjust(digits + 1, "0") + return sign + (text if not digits else f"{text[:-digits]}.{text[-digits:]}") + + +def normalize_money_tables( + tables: Mapping[str, pd.DataFrame], + declaration: Mapping, + *, + rate: Fraction, + rate_text: str, + factors: Mapping[str, Mapping], +) -> tuple[dict[tuple[str, str], np.ndarray], list[dict]]: + """Normalize every declared column once; return arrays and per-column receipts. + + ``factors`` maps each used family to ``{"identity": True, "reason"}`` or + ``{"factor_id", "numerator", "digits", "declaration_sha256", + "receipt_sha256"}``. A table already carrying a declared companion column + was normalized before and is refused, as is a NaN, an infinite value, a + negative amount in an unsigned concept, or a minor-unit total outside int64. + """ + if not isinstance(rate, Fraction) or rate <= 0: + raise ValueError("Money normalization needs a positive exact rate.") + digits = declaration["rounding"]["digits"] + scale = 10**digits + exchange = rate if declaration["quote"] == "to_per_from" else Fraction(1) / rate + minor_exchange = exchange * Fraction(scale) + suffix = declaration["cents_suffix"] + concepts = money_concept_columns() + families = normalized_column_families(declaration) + period = dict(declaration["donor_money_period"]) + arrays: dict[tuple[str, str], np.ndarray] = {} + receipts = [] + for (entity, column), family in families.items(): + table = tables[entity] + companion = f"{column}{suffix}" + if companion in table: + raise ValueError( + f"{entity}.{column} already carries {companion}; money is normalized " + "exactly once per lineage." + ) + if column not in table: + raise ValueError(f"Money column {entity}.{column} is absent.") + factor = factors[family] + identity = factor.get("identity") is True + ratio = ( + Fraction(1) + if identity + else Fraction(factor["numerator"], 10 ** factor["digits"]) + ) + signed = concepts[entity, column].monetary.signed + before = Fraction(0) + first, second = [], [] + for value in table[column].tolist(): + amount = _shortest_decimal(value) + if amount < 0 and not signed: + raise ValueError( + f"Unsigned money column {entity}.{column} holds a negative amount." + ) + before += amount + converted = _half_even(amount * minor_exchange) + aged = _half_even(Fraction(converted) * ratio) + if any( + not _INT64_MIN <= value <= _INT64_MAX for value in (converted, aged) + ): + raise ValueError( + f"Money column {entity}.{column} exceeds int64 minor units." + ) + first.append(converted) + second.append(aged) + # Residuals are linear: sum(m1 - x*q) = sum(m1) - q*sum(x), exactly. + fx_residual = Fraction(sum(first)) - before * minor_exchange + factor_residual = Fraction(sum(second)) - Fraction(sum(first)) * ratio + arrays[entity, column] = np.array( + [float(Fraction(units, scale)) for units in second], dtype=np.float64 + ) + arrays[entity, companion] = np.array(second, dtype=np.int64) + factor_receipt = ( + {"identity": True, "reason": factor["reason"]} + if factor.get("identity") is True + else { + "factor_id": factor["factor_id"], + "declaration_sha256": factor["declaration_sha256"], + "receipt_sha256": factor["receipt_sha256"], + "operational_factor": { + "numerator": factor["numerator"], + "denominator": 10 ** factor["digits"], + }, + } + ) + receipts.append( + { + "entity": entity, + "column": column, + "cents_column": companion, + "index_family": family, + "n": len(second), + "sum_before": _exact_decimal(before), + "sum_after_fx": _minor_units(first, digits), + "sum_after": _minor_units(second, digits), + "fx_rounding_residual_minor_units": _rational(fx_residual), + "factor_rounding_residual_minor_units": _rational(factor_residual), + "factor": factor_receipt, + "rate_text": rate_text, + "quote": declaration["quote"], + "donor_money_period": period, + } + ) + return arrays, receipts + + +def money_factor_inputs(entry: Mapping, declaration: Mapping) -> dict: + """The arithmetic inputs and identities of one checked, selected factor.""" + numerator, digits = check_money_factor(entry, declaration) + return { + "factor_id": entry["declaration"]["id"], + "numerator": numerator, + "digits": digits, + "declaration_sha256": factor_document_sha256(entry["declaration"]), + "receipt_sha256": entry["receipt"]["receipt_sha256"], + } diff --git a/packages/microcosm-build/src/microcosm/build/nz/country_package.json b/packages/microcosm-build/src/microcosm/build/nz/country_package.json index 2249271796..1bc736ee7b 100644 --- a/packages/microcosm-build/src/microcosm/build/nz/country_package.json +++ b/packages/microcosm-build/src/microcosm/build/nz/country_package.json @@ -92,6 +92,11 @@ "kind": "legacy_json", "schema_id": "legacy_json" }, + { + "path": "money_normalization.json", + "kind": "legacy_json", + "schema_id": "legacy_json" + }, { "path": "precal_references.json", "kind": "legacy_json", diff --git a/packages/microcosm-build/src/microcosm/build/nz/currency_bridge.json b/packages/microcosm-build/src/microcosm/build/nz/currency_bridge.json index c87a11e8fd..e2215358fe 100644 --- a/packages/microcosm-build/src/microcosm/build/nz/currency_bridge.json +++ b/packages/microcosm-build/src/microcosm/build/nz/currency_bridge.json @@ -2,29 +2,17 @@ "version": 1, "country": "nz", "method_card_row": "MC4", - "description": "The declared USD to NZD bridge for donor money concepts outside the quantile-map set (MC4 quantile-maps earnings, rent and liquid assets onto New Zealand distributions instead). It is exactly one multiplication with two-decimal rounding, to be applied before calibration and recorded in the build receipt. Aging from the donor dollar year to tax year 2026-27 is a separate declared index (MC1, MC5), not part of this bridge.", - "status": "awaiting D1 (MC4)", + "description": "The declared USD to NZD bridge for donor money concepts outside the quantile-map set (MC4 quantile-maps earnings, rent and liquid assets onto New Zealand distributions instead). It is exactly one multiplication with two-decimal rounding, applied before calibration and recorded in the build receipt. Aging from the donor dollar year to tax year 2026-27 is a separate declared index (MC1, MC5), not part of this bridge.", + "status": "Adopted: MC4 was adopted as written under d1043 (NZ hub, method card D1 as adopted, 2026-10-10; nz-hub notes/1010/method-card-D1-adopted.md). money_normalization.json is the single source of the executable rate, its quote, rounding and citation; this record points to it rather than repeating the rate.", "from_currency": "USD", "to_currency": "NZD", "operation": "multiply", - "rate": 1.654, - "rate_unit": "NZD per USD", - "rounding": { - "decimal_places": 2 + "rate": { + "resource": "money_normalization.json", + "path": [ + "rate" + ] }, "rate_year": 2024, - "rate_year_basis": "The donor dollar year: the pinned populace-us support artifact has survey period 2024 (source_stages.json artifact vintage \"2024\"; spec/vintages.yaml nz_support_pool_2024_buildp).", - "source": { - "publisher": "Internal Revenue Service", - "title": "Yearly average currency exchange rates", - "url": "https://www.irs.gov/individuals/international-taxpayers/yearly-average-currency-exchange-rates", - "table": "Yearly average exchange rates for converting foreign currencies into U.S. dollars", - "row": "New Zealand, Dollar", - "column": "2024", - "conversion_rule": "\"To convert from U.S. dollars to foreign currency, multiply the U.S. dollar amount by the applicable yearly average exchange rate in the table below.\"", - "page_last_reviewed": "2026-09-17", - "retrieved_on": "2026-10-07", - "retrieved_sha256": "8c765c8c88a6a6ad4fe325a0344ff69d2fa2ab174dcac2e8c13e1a5ea747fff8", - "retrieved_note": "SHA-256 of the HTML bytes fetched on the retrieval date; the page is re-rendered by the publisher, so it is a receipt, not a stable content key." - } + "rate_year_basis": "The donor dollar year: the pinned populace-us support artifact has survey period 2024 (source_stages.json artifact vintage \"2024\"; spec/vintages.yaml nz_support_pool_2024_buildp)." } diff --git a/packages/microcosm-build/src/microcosm/build/nz/money_normalization.json b/packages/microcosm-build/src/microcosm/build/nz/money_normalization.json new file mode 100644 index 0000000000..2b244bbb7f --- /dev/null +++ b/packages/microcosm-build/src/microcosm/build/nz/money_normalization.json @@ -0,0 +1,125 @@ +{ + "schema_version": 1, + "description": "Exact normalization of the donor's money columns to New Zealand dollars at the tax year 2026-27 money level (MC4, MC5). Each normalized column is converted once at the declared rate and then aged once by its index family's donor factor, both rounded half-even to cents. Earnings, rent and liquid assets are quantile-mapped onto New Zealand distributions instead (MC4) and are never converted at the rate.", + "status": "Declared, not active: no New Zealand transport graph reads it yet (G8-w3).", + "from_currency": "USD", + "to_currency": "NZD", + "rate": { + "form": "decimal_text", + "text": "1.654", + "citation": { + "publisher": "Internal Revenue Service", + "title": "Yearly average currency exchange rates", + "url": "https://www.irs.gov/individuals/international-taxpayers/yearly-average-currency-exchange-rates", + "table": "Yearly average exchange rates for converting foreign currencies into U.S. dollars", + "row": "New Zealand, Dollar", + "column": "2024", + "conversion_rule": "\"To convert from U.S. dollars to foreign currency, multiply the U.S. dollar amount by the applicable yearly average exchange rate in the table below.\"", + "page_last_reviewed": "2026-09-17", + "retrieved_on": "2026-10-07", + "retrieved_sha256": "8c765c8c88a6a6ad4fe325a0344ff69d2fa2ab174dcac2e8c13e1a5ea747fff8", + "retrieved_note": "SHA-256 of the HTML bytes fetched on the retrieval date; the page is re-rendered by the publisher, so it is a receipt, not a stable content key." + } + }, + "quote": "to_per_from", + "rounding": { + "digits": 2, + "mode": "half_even" + }, + "donor_money_period": { + "type": "calendar_year", + "value": 2024, + "basis": "declared_convention", + "evidence": [ + "tools/build_us_fiscal_refresh_release.py:410 declares PERIOD = 2024; lines 4320-4328 pass PERIOD as time_period to USSingleYearDataset.", + "packages/microcosm-frame/src/microcosm/frame/adapters/policyengine_us.py:1230-1234 writes the supplied model period as a one-row integer _time_period table. This is model-period lineage evidence, not authentication of the monetary basis of donor amounts." + ] + }, + "columns": { + "person": [ + "nonfarm_self_employment_income", + "farm_self_employment_income", + "interest_income", + "dividend_income", + "rental_income", + "realized_capital_gains", + "private_pension_income" + ], + "household": [ + "mortgage_interest", + "mortgage_principal", + "property_tax" + ] + }, + "factor_profile": { + "dimension": "aging_index", + "profiles": [ + "low", + "central", + "high" + ], + "central_profile": "central" + }, + "index_family_factors": { + "mixed_income": { + "factors": { + "low": "donor_cy2024_to_ty2027_low", + "central": "donor_cy2024_to_ty2027_central", + "high": "donor_cy2024_to_ty2027_high" + }, + "disclosure": "Self-employment income (both columns, signed) is aged by Stats NZ QES ordinary-time average weekly earnings growth, the AWE convention; provisional pending Max's ruling on non-wage ageing (H3). No separate mixed-income index is used." + }, + "capital_income": { + "factors": { + "low": "donor_cy2024_to_ty2027_low", + "central": "donor_cy2024_to_ty2027_central", + "high": "donor_cy2024_to_ty2027_high" + }, + "disclosure": "Interest, dividends, rental income and realized capital gains are aged by AWE growth; provisional pending Max's ruling on non-wage ageing (H3). No asset-return or price index is used." + }, + "pensions": { + "factors": { + "low": "donor_cy2024_to_ty2027_low", + "central": "donor_cy2024_to_ty2027_central", + "high": "donor_cy2024_to_ty2027_high" + }, + "disclosure": "Private pension income is aged by AWE growth; provisional pending Max's ruling on non-wage ageing (H3)." + }, + "owner_housing_costs": { + "factors": { + "low": "donor_cy2024_to_ty2027_low", + "central": "donor_cy2024_to_ty2027_central", + "high": "donor_cy2024_to_ty2027_high" + }, + "disclosure": "Mortgage interest, mortgage principal and property tax are aged by AWE growth as an interim index; provisional pending Max's ruling on non-wage ageing (H3)." + } + }, + "cents_suffix": "_cents", + "quantile_mapped": { + "person": { + "employment_income": { + "awaits": [ + "P1: precal_references ird_wage_salary_people_by_band rows 7 to 241" + ], + "reason": "MC4 quantile-maps earnings onto IRD wage bands; P1 is parked until its source-period wage bridge and intervals exist (d1952 option A, earnings first)." + }, + "liquid_financial_assets": { + "awaits": [ + "P3: precal_references stats_nz_household_financial_net_worth_distribution", + "C2: an all-household liquid-asset distribution" + ], + "reason": "MC4 quantile-maps liquid assets; no all-household distribution exists yet (C2), and any interim producer is Max's call (M1)." + } + }, + "household": { + "rent": { + "awaits": [ + "C1: Census 2023 household weekly rent bands (G8-d1)" + ], + "reason": "MC4 quantile-maps rent onto the Census 2023 weekly-rent bands for renting households (C1); the MBIE bond reference P2 is parked." + } + } + }, + "pending": {}, + "notes": "The rate is the IRS 2024 yearly average for the New Zealand dollar, quoted in NZD per USD, so a donor amount is multiplied by it. Calendar-2024 money is a declared convention covering flows and asset stocks; the donor monetary basis has not been authenticated against a producing manifest. The US build's PERIOD = 2024 and the exporter's _time_period write provide model-period lineage evidence. Money normalization refuses a donor whose recorded _time_period differs from calendar 2024 (G8-w H1); this runtime check is a consistency check, not authentication. No donor file is opened to establish this declaration. Variants V1 and V2 age with their own aging_index profile (G8-w H2). public_pension_income is dropped at CREATE and is not declared." +} diff --git a/packages/microcosm-build/src/microcosm/build/nz/reference_aging.json b/packages/microcosm-build/src/microcosm/build/nz/reference_aging.json index a82957f2a7..34a1af66e8 100644 --- a/packages/microcosm-build/src/microcosm/build/nz/reference_aging.json +++ b/packages/microcosm-build/src/microcosm/build/nz/reference_aging.json @@ -8381,6 +8381,3612 @@ ], "receipt_sha256": "6487bd1664ce1e691faaa0771e9835da5437243b06a5c7d749888380a76cd995" } + }, + { + "declaration": { + "schema_version": 1, + "id": "donor_cy2024_to_ty2027_low", + "operation": "projected_period_average_ratio", + "operation_version": 1, + "purpose": "money_normalization_input", + "feed_identity": { + "source_commit": "8f4cfd573c0d6f580d9c51cd67e8f449a42ed174", + "facts_sha256": "03f5ea0fe60e6dc93a135ab32cd5cd6ffb56388e702a4734cdb515a956e36b18", + "manifest_sha256": "1ea2d1eeb122275a9ce7f861ca3e20e21dd695772c04ce9d71114168e92fe053", + "artifact_schema_version": "policyengine_ledger.consumer_artifact.v2", + "consumer_fact_schema_versions": [ + "chronicle.consumer_fact.v4" + ], + "consumer_fact_schema_sha256": "d953306d6f01fbc64db4372d0262d3012ee590269f8a25b276b18dca0626c6a0", + "fact_row_count": 12368 + }, + "source_period": "CY2024", + "target_period": 2027, + "source_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2024-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2024q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:854e91ec1dd95e396659ad8e", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2024-01-01", + "end_date": "2024-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2024-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2024q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:c07fd9ce3137fa20386bafe0", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2024-04-01", + "end_date": "2024-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 1 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2024-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2024q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:54a0eeb64049ba4c15e397bf", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2024-07-01", + "end_date": "2024-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 2 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2024-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2024q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:7fc35ba9682ceb04504f2ef9", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2024-10-01", + "end_date": "2024-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 3 + } + ], + "target_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-04-01", + "end_date": "2026-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 0, + "projection_offset": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2026-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 1, + "projection_offset": 1 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-10-01", + "end_date": "2026-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 2, + "projection_offset": 2 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2027-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2027q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2027-01-01", + "end_date": "2027-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 3, + "projection_offset": 3 + } + ], + "growth": { + "selector": { + "source_name": "treasury", + "source_measure_id": "average_weekly_earnings_annual_percentage_change", + "source_concept": "treasury.prefu26.average_weekly_earnings_annual_percentage_change", + "entity_name": "government", + "period_type": "fiscal_year", + "period_value": 2026, + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "treasury.prefu26.average_weekly_earnings.fy2026", + "dimensions": [ + "earnings_component", + "publication_status" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "publication_status": "Forecast" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:aba7d3d54489b068116e6a25", + "assertion": "source_projection", + "source_table": "PREFU 2026 Table 4.1 - Key economic assumptions for fiscal forecasts" + }, + "validation": { + "unit": "percent", + "source_sha256": "e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9", + "source_vintage": "prefu_2026_09_29", + "entity_role": "forecaster", + "provenance_class": "model_output", + "assertion": "source_projection", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2027-06-30" + }, + "source_column_id": "year_ending_30_june_2027" + }, + "unit_multiplier": "0.01", + "scenario_delta": "-0.5", + "assertion_policy": "allow_source_projection", + "sensitivity_provenance": [ + "d1599: symmetric ±0.5 percentage-point Microcosm sensitivity; high is not a Treasury forecast path" + ] + }, + "subperiods_per_year": 4, + "projection_policy": "observed_preferred_latest_contiguous", + "rounding": { + "digits": 6, + "mode": "nearest_half_even" + }, + "expected_operational_factor": { + "numerator": 1077090, + "denominator": 1000000 + }, + "validity": { + "declared_on": "2026-10-23", + "valid_from": "2026-10-23", + "valid_until": "2026-11-03", + "as_of": "2026-10-23" + }, + "evidence": [ + "Stats NZ QES Table 7; QEMQ.SBSZ9A; ordinary-time FTE, all sectors, actual", + "Treasury PREFU 2026 Table 4.1; year ending 30 June 2027; Chronicle period 2026", + "Declared calendar-2024 money convention (H1), covering flows and asset stocks; the recorded _time_period is a consistency check, not authentication" + ], + "decision_references": [ + "d1952 card; NZ money year build item: donor money is calendar-2024 dollars", + "G8-w hub rulings 5, 24 October 2026, items H1–H3", + "G8-r2 hub rulings 3, 23 October 2026, section 1" + ] + }, + "operational_factor": { + "numerator": 1077090, + "denominator": 1000000 + }, + "receipt": { + "schema_version": 1, + "operation": "projected_period_average_ratio", + "operation_version": 1, + "factor_id": "donor_cy2024_to_ty2027_low", + "declaration_sha256": "814ca4fd84c53431bd05e4db9127a6567923f73f56a149a685424f75a14bcdfe", + "feed_identity": { + "source_commit": "8f4cfd573c0d6f580d9c51cd67e8f449a42ed174", + "facts_sha256": "03f5ea0fe60e6dc93a135ab32cd5cd6ffb56388e702a4734cdb515a956e36b18", + "manifest_sha256": "1ea2d1eeb122275a9ce7f861ca3e20e21dd695772c04ce9d71114168e92fe053", + "artifact_schema_version": "policyengine_ledger.consumer_artifact.v2", + "consumer_fact_schema_versions": [ + "chronicle.consumer_fact.v4" + ], + "consumer_fact_schema_sha256": "d953306d6f01fbc64db4372d0262d3012ee590269f8a25b276b18dca0626c6a0", + "fact_row_count": 12368 + }, + "source_period": "CY2024", + "target_period": 2027, + "source_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2024-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2024q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:854e91ec1dd95e396659ad8e", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:854e91ec1dd95e396659ad8e", + "decimal_operand": "1563.54", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1563.54", + "recovered_decimal": "1563.54", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2024-01-01", + "end_date": "2024-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2024-Q1" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2025-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_size_bytes": 135784, + "source_table": "Quarterly Employment Survey June 2025 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2025-quarter/Download-data/quarterly-employment-survey-june-2025-quarter.xlsx", + "vintage": "stats_nz_qes_june_2025" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2024-03-31", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Mar 2024 quarter", + "start_date": "2024-01-01" + }, + "weight": "1", + "sequence": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2024-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2024q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:c07fd9ce3137fa20386bafe0", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:c07fd9ce3137fa20386bafe0", + "decimal_operand": "1586.32", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1586.32", + "recovered_decimal": "1586.32", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2024-04-01", + "end_date": "2024-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2024-Q2" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2024-06-30", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Jun 2024 quarter", + "start_date": "2024-04-01" + }, + "weight": "1", + "sequence": 1 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2024-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2024q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:54a0eeb64049ba4c15e397bf", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:54a0eeb64049ba4c15e397bf", + "decimal_operand": "1595.11", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1595.11", + "recovered_decimal": "1595.11", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2024-07-01", + "end_date": "2024-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2024-Q3" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2024-09-30", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Sep 2024 quarter", + "start_date": "2024-07-01" + }, + "weight": "1", + "sequence": 2 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2024-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2024q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:7fc35ba9682ceb04504f2ef9", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:7fc35ba9682ceb04504f2ef9", + "decimal_operand": "1622.73", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1622.73", + "recovered_decimal": "1622.73", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2024-10-01", + "end_date": "2024-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2024-Q4" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2024-12-31", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Dec 2024 quarter", + "start_date": "2024-10-01" + }, + "weight": "1", + "sequence": 3 + } + ], + "target_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2", + "decimal_operand": "1700.68", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1700.68", + "recovered_decimal": "1700.68", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-04-01", + "end_date": "2026-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2026-Q2" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2026-06-30", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Jun 2026 quarter", + "start_date": "2026-04-01" + }, + "status": "observed", + "sequence": 0, + "weight": "1", + "projection_offset": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2026-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "status": "projected", + "sequence": 1, + "weight": "1", + "projection_offset": 1, + "projection_anchor": { + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2" + } + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-10-01", + "end_date": "2026-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "status": "projected", + "sequence": 2, + "weight": "1", + "projection_offset": 2, + "projection_anchor": { + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2" + } + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2027-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2027q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2027-01-01", + "end_date": "2027-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "status": "projected", + "sequence": 3, + "weight": "1", + "projection_offset": 3, + "projection_anchor": { + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2" + } + } + ], + "subperiods_per_year": 4, + "projection_policy": "observed_preferred_latest_contiguous", + "denominator_mean": { + "numerator": 63677, + "denominator": 40 + }, + "growth": { + "selector": { + "source_name": "treasury", + "source_measure_id": "average_weekly_earnings_annual_percentage_change", + "source_concept": "treasury.prefu26.average_weekly_earnings_annual_percentage_change", + "entity_name": "government", + "period_type": "fiscal_year", + "period_value": 2026, + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "treasury.prefu26.average_weekly_earnings.fy2026", + "dimensions": [ + "earnings_component", + "publication_status" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "publication_status": "Forecast" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:aba7d3d54489b068116e6a25", + "assertion": "source_projection", + "source_table": "PREFU 2026 Table 4.1 - Key economic assumptions for fiscal forecasts" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:aba7d3d54489b068116e6a25", + "decimal_operand": "2.7", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "2.7", + "recovered_decimal": "2.7", + "decimal_values_equal": true + }, + "validation": { + "unit": "percent", + "source_sha256": "e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9", + "source_vintage": "prefu_2026_09_29", + "entity_role": "forecaster", + "provenance_class": "model_output", + "assertion": "source_projection", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2027-06-30" + }, + "source_column_id": "year_ending_30_june_2027" + }, + "period": { + "type": "fiscal_year", + "value": 2026 + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/treasury/treasury-prefu26-economic-assumptions/2026/e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9/prefu26-charts-data.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/treasury/treasury-prefu26-economic-assumptions/2026/e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9/prefu26-charts-data.xlsx", + "source_file": "prefu26-charts-data.xlsx", + "source_name": "treasury", + "source_sha256": "e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9", + "source_size_bytes": 664670, + "source_table": "PREFU 2026 Table 4.1 - Key economic assumptions for fiscal forecasts", + "url": "https://www.treasury.govt.nz/sites/default/files/2026-09/prefu26-charts-data.xlsx", + "vintage": "prefu_2026_09_29" + }, + "unit": "percent", + "assertion": "source_projection", + "provenance_class": "model_output", + "entity_role": "forecaster", + "period_coverage": { + "basis": "fiscal", + "end_date": "2027-06-30", + "notes": "Publisher Year ending 30 June 2027; Chronicle opening-year fiscal_year:2026. Publisher column status is Forecast. Footnote 3 (Table 4.1!C21:D21): Ordinary time. Footnote 4 (C22:D22): Measures are actuals except for real GDP and nominal GDP. 2026 earnings are therefore actuals despite the Forecast4 column heading; only 2027–2031 earnings are forecasts. The published annual percentage change is retained unchanged; no annual average, level, factor or period alignment is calculated.", + "source_period_label": "Year ending 30 June 2027", + "start_date": "2026-07-01" + }, + "unit_multiplier": "0.01", + "scenario_delta": "-0.5", + "sensitivity_provenance": [ + "d1599: symmetric ±0.5 percentage-point Microcosm sensitivity; high is not a Treasury forecast path" + ], + "effective_growth": { + "numerator": 11, + "denominator": 500 + } + }, + "root_bounds": { + "lower": { + "numerator": 50272759932393, + "denominator": 50000000000000 + }, + "upper": { + "numerator": 100545519864787, + "denominator": 100000000000000 + }, + "exact": false, + "decimal_places": 14, + "degree": 4, + "radicand": { + "numerator": 511, + "denominator": 500 + } + }, + "numerator_mean_bounds": { + "lower": { + "numerator": 21433087509261019469100089110351817978329164269, + "denominator": 12500000000000000000000000000000000000000000 + }, + "upper": { + "numerator": 171464700074090725365853333139754513013059781351, + "denominator": 100000000000000000000000000000000000000000000 + } + }, + "unrounded_factor_bounds": { + "lower": { + "numerator": 21433087509261019469100089110351817978329164269, + "denominator": 19899062500000000000000000000000000000000000000 + }, + "upper": { + "numerator": 171464700074090725365853333139754513013059781351, + "denominator": 159192500000000000000000000000000000000000000000 + } + }, + "rounding": { + "digits": 6, + "mode": "nearest_half_even" + }, + "operational_factor": { + "numerator": 1077090, + "denominator": 1000000 + }, + "validity": { + "declared_on": "2026-10-23", + "valid_from": "2026-10-23", + "valid_until": "2026-11-03", + "as_of": "2026-10-23" + }, + "evidence": [ + "Stats NZ QES Table 7; QEMQ.SBSZ9A; ordinary-time FTE, all sectors, actual", + "Treasury PREFU 2026 Table 4.1; year ending 30 June 2027; Chronicle period 2026", + "Declared calendar-2024 money convention (H1), covering flows and asset stocks; the recorded _time_period is a consistency check, not authentication" + ], + "decision_references": [ + "d1952 card; NZ money year build item: donor money is calendar-2024 dollars", + "G8-w hub rulings 5, 24 October 2026, items H1–H3", + "G8-r2 hub rulings 3, 23 October 2026, section 1" + ], + "receipt_sha256": "e6a3adeec37249a70142c7801bc031915a0f40d9521b91c567fbf4a11e616ada" + } + }, + { + "declaration": { + "schema_version": 1, + "id": "donor_cy2024_to_ty2027_central", + "operation": "projected_period_average_ratio", + "operation_version": 1, + "purpose": "money_normalization_input", + "feed_identity": { + "source_commit": "8f4cfd573c0d6f580d9c51cd67e8f449a42ed174", + "facts_sha256": "03f5ea0fe60e6dc93a135ab32cd5cd6ffb56388e702a4734cdb515a956e36b18", + "manifest_sha256": "1ea2d1eeb122275a9ce7f861ca3e20e21dd695772c04ce9d71114168e92fe053", + "artifact_schema_version": "policyengine_ledger.consumer_artifact.v2", + "consumer_fact_schema_versions": [ + "chronicle.consumer_fact.v4" + ], + "consumer_fact_schema_sha256": "d953306d6f01fbc64db4372d0262d3012ee590269f8a25b276b18dca0626c6a0", + "fact_row_count": 12368 + }, + "source_period": "CY2024", + "target_period": 2027, + "source_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2024-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2024q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:854e91ec1dd95e396659ad8e", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2024-01-01", + "end_date": "2024-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2024-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2024q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:c07fd9ce3137fa20386bafe0", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2024-04-01", + "end_date": "2024-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 1 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2024-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2024q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:54a0eeb64049ba4c15e397bf", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2024-07-01", + "end_date": "2024-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 2 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2024-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2024q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:7fc35ba9682ceb04504f2ef9", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2024-10-01", + "end_date": "2024-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 3 + } + ], + "target_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-04-01", + "end_date": "2026-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 0, + "projection_offset": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2026-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 1, + "projection_offset": 1 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-10-01", + "end_date": "2026-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 2, + "projection_offset": 2 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2027-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2027q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2027-01-01", + "end_date": "2027-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 3, + "projection_offset": 3 + } + ], + "growth": { + "selector": { + "source_name": "treasury", + "source_measure_id": "average_weekly_earnings_annual_percentage_change", + "source_concept": "treasury.prefu26.average_weekly_earnings_annual_percentage_change", + "entity_name": "government", + "period_type": "fiscal_year", + "period_value": 2026, + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "treasury.prefu26.average_weekly_earnings.fy2026", + "dimensions": [ + "earnings_component", + "publication_status" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "publication_status": "Forecast" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:aba7d3d54489b068116e6a25", + "assertion": "source_projection", + "source_table": "PREFU 2026 Table 4.1 - Key economic assumptions for fiscal forecasts" + }, + "validation": { + "unit": "percent", + "source_sha256": "e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9", + "source_vintage": "prefu_2026_09_29", + "entity_role": "forecaster", + "provenance_class": "model_output", + "assertion": "source_projection", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2027-06-30" + }, + "source_column_id": "year_ending_30_june_2027" + }, + "unit_multiplier": "0.01", + "scenario_delta": "0", + "assertion_policy": "allow_source_projection", + "sensitivity_provenance": [ + "d1599: symmetric ±0.5 percentage-point Microcosm sensitivity; high is not a Treasury forecast path" + ] + }, + "subperiods_per_year": 4, + "projection_policy": "observed_preferred_latest_contiguous", + "rounding": { + "digits": 6, + "mode": "nearest_half_even" + }, + "expected_operational_factor": { + "numerator": 1079073, + "denominator": 1000000 + }, + "validity": { + "declared_on": "2026-10-23", + "valid_from": "2026-10-23", + "valid_until": "2026-11-03", + "as_of": "2026-10-23" + }, + "evidence": [ + "Stats NZ QES Table 7; QEMQ.SBSZ9A; ordinary-time FTE, all sectors, actual", + "Treasury PREFU 2026 Table 4.1; year ending 30 June 2027; Chronicle period 2026", + "Declared calendar-2024 money convention (H1), covering flows and asset stocks; the recorded _time_period is a consistency check, not authentication" + ], + "decision_references": [ + "d1952 card; NZ money year build item: donor money is calendar-2024 dollars", + "G8-w hub rulings 5, 24 October 2026, items H1–H3", + "G8-r2 hub rulings 3, 23 October 2026, section 1" + ] + }, + "operational_factor": { + "numerator": 1079073, + "denominator": 1000000 + }, + "receipt": { + "schema_version": 1, + "operation": "projected_period_average_ratio", + "operation_version": 1, + "factor_id": "donor_cy2024_to_ty2027_central", + "declaration_sha256": "cb026f66df5cc07468a0c517e348b399082f0648b81856b4ca888587ba8d9d25", + "feed_identity": { + "source_commit": "8f4cfd573c0d6f580d9c51cd67e8f449a42ed174", + "facts_sha256": "03f5ea0fe60e6dc93a135ab32cd5cd6ffb56388e702a4734cdb515a956e36b18", + "manifest_sha256": "1ea2d1eeb122275a9ce7f861ca3e20e21dd695772c04ce9d71114168e92fe053", + "artifact_schema_version": "policyengine_ledger.consumer_artifact.v2", + "consumer_fact_schema_versions": [ + "chronicle.consumer_fact.v4" + ], + "consumer_fact_schema_sha256": "d953306d6f01fbc64db4372d0262d3012ee590269f8a25b276b18dca0626c6a0", + "fact_row_count": 12368 + }, + "source_period": "CY2024", + "target_period": 2027, + "source_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2024-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2024q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:854e91ec1dd95e396659ad8e", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:854e91ec1dd95e396659ad8e", + "decimal_operand": "1563.54", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1563.54", + "recovered_decimal": "1563.54", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2024-01-01", + "end_date": "2024-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2024-Q1" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2025-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_size_bytes": 135784, + "source_table": "Quarterly Employment Survey June 2025 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2025-quarter/Download-data/quarterly-employment-survey-june-2025-quarter.xlsx", + "vintage": "stats_nz_qes_june_2025" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2024-03-31", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Mar 2024 quarter", + "start_date": "2024-01-01" + }, + "weight": "1", + "sequence": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2024-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2024q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:c07fd9ce3137fa20386bafe0", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:c07fd9ce3137fa20386bafe0", + "decimal_operand": "1586.32", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1586.32", + "recovered_decimal": "1586.32", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2024-04-01", + "end_date": "2024-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2024-Q2" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2024-06-30", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Jun 2024 quarter", + "start_date": "2024-04-01" + }, + "weight": "1", + "sequence": 1 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2024-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2024q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:54a0eeb64049ba4c15e397bf", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:54a0eeb64049ba4c15e397bf", + "decimal_operand": "1595.11", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1595.11", + "recovered_decimal": "1595.11", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2024-07-01", + "end_date": "2024-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2024-Q3" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2024-09-30", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Sep 2024 quarter", + "start_date": "2024-07-01" + }, + "weight": "1", + "sequence": 2 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2024-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2024q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:7fc35ba9682ceb04504f2ef9", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:7fc35ba9682ceb04504f2ef9", + "decimal_operand": "1622.73", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1622.73", + "recovered_decimal": "1622.73", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2024-10-01", + "end_date": "2024-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2024-Q4" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2024-12-31", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Dec 2024 quarter", + "start_date": "2024-10-01" + }, + "weight": "1", + "sequence": 3 + } + ], + "target_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2", + "decimal_operand": "1700.68", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1700.68", + "recovered_decimal": "1700.68", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-04-01", + "end_date": "2026-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2026-Q2" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2026-06-30", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Jun 2026 quarter", + "start_date": "2026-04-01" + }, + "status": "observed", + "sequence": 0, + "weight": "1", + "projection_offset": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2026-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "status": "projected", + "sequence": 1, + "weight": "1", + "projection_offset": 1, + "projection_anchor": { + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2" + } + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-10-01", + "end_date": "2026-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "status": "projected", + "sequence": 2, + "weight": "1", + "projection_offset": 2, + "projection_anchor": { + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2" + } + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2027-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2027q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2027-01-01", + "end_date": "2027-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "status": "projected", + "sequence": 3, + "weight": "1", + "projection_offset": 3, + "projection_anchor": { + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2" + } + } + ], + "subperiods_per_year": 4, + "projection_policy": "observed_preferred_latest_contiguous", + "denominator_mean": { + "numerator": 63677, + "denominator": 40 + }, + "growth": { + "selector": { + "source_name": "treasury", + "source_measure_id": "average_weekly_earnings_annual_percentage_change", + "source_concept": "treasury.prefu26.average_weekly_earnings_annual_percentage_change", + "entity_name": "government", + "period_type": "fiscal_year", + "period_value": 2026, + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "treasury.prefu26.average_weekly_earnings.fy2026", + "dimensions": [ + "earnings_component", + "publication_status" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "publication_status": "Forecast" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:aba7d3d54489b068116e6a25", + "assertion": "source_projection", + "source_table": "PREFU 2026 Table 4.1 - Key economic assumptions for fiscal forecasts" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:aba7d3d54489b068116e6a25", + "decimal_operand": "2.7", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "2.7", + "recovered_decimal": "2.7", + "decimal_values_equal": true + }, + "validation": { + "unit": "percent", + "source_sha256": "e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9", + "source_vintage": "prefu_2026_09_29", + "entity_role": "forecaster", + "provenance_class": "model_output", + "assertion": "source_projection", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2027-06-30" + }, + "source_column_id": "year_ending_30_june_2027" + }, + "period": { + "type": "fiscal_year", + "value": 2026 + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/treasury/treasury-prefu26-economic-assumptions/2026/e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9/prefu26-charts-data.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/treasury/treasury-prefu26-economic-assumptions/2026/e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9/prefu26-charts-data.xlsx", + "source_file": "prefu26-charts-data.xlsx", + "source_name": "treasury", + "source_sha256": "e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9", + "source_size_bytes": 664670, + "source_table": "PREFU 2026 Table 4.1 - Key economic assumptions for fiscal forecasts", + "url": "https://www.treasury.govt.nz/sites/default/files/2026-09/prefu26-charts-data.xlsx", + "vintage": "prefu_2026_09_29" + }, + "unit": "percent", + "assertion": "source_projection", + "provenance_class": "model_output", + "entity_role": "forecaster", + "period_coverage": { + "basis": "fiscal", + "end_date": "2027-06-30", + "notes": "Publisher Year ending 30 June 2027; Chronicle opening-year fiscal_year:2026. Publisher column status is Forecast. Footnote 3 (Table 4.1!C21:D21): Ordinary time. Footnote 4 (C22:D22): Measures are actuals except for real GDP and nominal GDP. 2026 earnings are therefore actuals despite the Forecast4 column heading; only 2027–2031 earnings are forecasts. The published annual percentage change is retained unchanged; no annual average, level, factor or period alignment is calculated.", + "source_period_label": "Year ending 30 June 2027", + "start_date": "2026-07-01" + }, + "unit_multiplier": "0.01", + "scenario_delta": "0", + "sensitivity_provenance": [ + "d1599: symmetric ±0.5 percentage-point Microcosm sensitivity; high is not a Treasury forecast path" + ], + "effective_growth": { + "numerator": 27, + "denominator": 1000 + } + }, + "root_bounds": { + "lower": { + "numerator": 100668271307927, + "denominator": 100000000000000 + }, + "upper": { + "numerator": 12583533913491, + "denominator": 12500000000000 + }, + "exact": false, + "decimal_places": 14, + "degree": 4, + "radicand": { + "numerator": 1027, + "denominator": 1000 + } + }, + "numerator_mean_bounds": { + "lower": { + "numerator": 171780381168748188591006079321988113950935096211, + "denominator": 100000000000000000000000000000000000000000000 + }, + "upper": { + "numerator": 335508556970216332809143747140199880884173607, + "denominator": 195312500000000000000000000000000000000000 + } + }, + "unrounded_factor_bounds": { + "lower": { + "numerator": 171780381168748188591006079321988113950935096211, + "denominator": 159192500000000000000000000000000000000000000000 + }, + "upper": { + "numerator": 335508556970216332809143747140199880884173607, + "denominator": 310922851562500000000000000000000000000000000 + } + }, + "rounding": { + "digits": 6, + "mode": "nearest_half_even" + }, + "operational_factor": { + "numerator": 1079073, + "denominator": 1000000 + }, + "validity": { + "declared_on": "2026-10-23", + "valid_from": "2026-10-23", + "valid_until": "2026-11-03", + "as_of": "2026-10-23" + }, + "evidence": [ + "Stats NZ QES Table 7; QEMQ.SBSZ9A; ordinary-time FTE, all sectors, actual", + "Treasury PREFU 2026 Table 4.1; year ending 30 June 2027; Chronicle period 2026", + "Declared calendar-2024 money convention (H1), covering flows and asset stocks; the recorded _time_period is a consistency check, not authentication" + ], + "decision_references": [ + "d1952 card; NZ money year build item: donor money is calendar-2024 dollars", + "G8-w hub rulings 5, 24 October 2026, items H1–H3", + "G8-r2 hub rulings 3, 23 October 2026, section 1" + ], + "receipt_sha256": "ce934982c900e9996911b0a8d5b0de52d567c99124d33b597ea9fb481df45db1" + } + }, + { + "declaration": { + "schema_version": 1, + "id": "donor_cy2024_to_ty2027_high", + "operation": "projected_period_average_ratio", + "operation_version": 1, + "purpose": "money_normalization_input", + "feed_identity": { + "source_commit": "8f4cfd573c0d6f580d9c51cd67e8f449a42ed174", + "facts_sha256": "03f5ea0fe60e6dc93a135ab32cd5cd6ffb56388e702a4734cdb515a956e36b18", + "manifest_sha256": "1ea2d1eeb122275a9ce7f861ca3e20e21dd695772c04ce9d71114168e92fe053", + "artifact_schema_version": "policyengine_ledger.consumer_artifact.v2", + "consumer_fact_schema_versions": [ + "chronicle.consumer_fact.v4" + ], + "consumer_fact_schema_sha256": "d953306d6f01fbc64db4372d0262d3012ee590269f8a25b276b18dca0626c6a0", + "fact_row_count": 12368 + }, + "source_period": "CY2024", + "target_period": 2027, + "source_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2024-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2024q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:854e91ec1dd95e396659ad8e", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2024-01-01", + "end_date": "2024-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2024-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2024q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:c07fd9ce3137fa20386bafe0", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2024-04-01", + "end_date": "2024-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 1 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2024-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2024q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:54a0eeb64049ba4c15e397bf", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2024-07-01", + "end_date": "2024-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 2 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2024-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2024q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:7fc35ba9682ceb04504f2ef9", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2024-10-01", + "end_date": "2024-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 3 + } + ], + "target_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2", + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-04-01", + "end_date": "2026-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 0, + "projection_offset": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2026-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 1, + "projection_offset": 1 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-10-01", + "end_date": "2026-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 2, + "projection_offset": 2 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2027-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2027q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2027-01-01", + "end_date": "2027-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "weight": "1", + "sequence": 3, + "projection_offset": 3 + } + ], + "growth": { + "selector": { + "source_name": "treasury", + "source_measure_id": "average_weekly_earnings_annual_percentage_change", + "source_concept": "treasury.prefu26.average_weekly_earnings_annual_percentage_change", + "entity_name": "government", + "period_type": "fiscal_year", + "period_value": 2026, + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "treasury.prefu26.average_weekly_earnings.fy2026", + "dimensions": [ + "earnings_component", + "publication_status" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "publication_status": "Forecast" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:aba7d3d54489b068116e6a25", + "assertion": "source_projection", + "source_table": "PREFU 2026 Table 4.1 - Key economic assumptions for fiscal forecasts" + }, + "validation": { + "unit": "percent", + "source_sha256": "e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9", + "source_vintage": "prefu_2026_09_29", + "entity_role": "forecaster", + "provenance_class": "model_output", + "assertion": "source_projection", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2027-06-30" + }, + "source_column_id": "year_ending_30_june_2027" + }, + "unit_multiplier": "0.01", + "scenario_delta": "0.5", + "assertion_policy": "allow_source_projection", + "sensitivity_provenance": [ + "d1599: symmetric ±0.5 percentage-point Microcosm sensitivity; high is not a Treasury forecast path" + ] + }, + "subperiods_per_year": 4, + "projection_policy": "observed_preferred_latest_contiguous", + "rounding": { + "digits": 6, + "mode": "nearest_half_even" + }, + "expected_operational_factor": { + "numerator": 1081052, + "denominator": 1000000 + }, + "validity": { + "declared_on": "2026-10-23", + "valid_from": "2026-10-23", + "valid_until": "2026-11-03", + "as_of": "2026-10-23" + }, + "evidence": [ + "Stats NZ QES Table 7; QEMQ.SBSZ9A; ordinary-time FTE, all sectors, actual", + "Treasury PREFU 2026 Table 4.1; year ending 30 June 2027; Chronicle period 2026", + "Declared calendar-2024 money convention (H1), covering flows and asset stocks; the recorded _time_period is a consistency check, not authentication" + ], + "decision_references": [ + "d1952 card; NZ money year build item: donor money is calendar-2024 dollars", + "G8-w hub rulings 5, 24 October 2026, items H1–H3", + "G8-r2 hub rulings 3, 23 October 2026, section 1" + ] + }, + "operational_factor": { + "numerator": 1081052, + "denominator": 1000000 + }, + "receipt": { + "schema_version": 1, + "operation": "projected_period_average_ratio", + "operation_version": 1, + "factor_id": "donor_cy2024_to_ty2027_high", + "declaration_sha256": "2dea33aa96b4a553780bf9f7b361b48a1487470c710a080b79330e8db3793bbd", + "feed_identity": { + "source_commit": "8f4cfd573c0d6f580d9c51cd67e8f449a42ed174", + "facts_sha256": "03f5ea0fe60e6dc93a135ab32cd5cd6ffb56388e702a4734cdb515a956e36b18", + "manifest_sha256": "1ea2d1eeb122275a9ce7f861ca3e20e21dd695772c04ce9d71114168e92fe053", + "artifact_schema_version": "policyengine_ledger.consumer_artifact.v2", + "consumer_fact_schema_versions": [ + "chronicle.consumer_fact.v4" + ], + "consumer_fact_schema_sha256": "d953306d6f01fbc64db4372d0262d3012ee590269f8a25b276b18dca0626c6a0", + "fact_row_count": 12368 + }, + "source_period": "CY2024", + "target_period": 2027, + "source_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2024-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2024q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:854e91ec1dd95e396659ad8e", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:854e91ec1dd95e396659ad8e", + "decimal_operand": "1563.54", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1563.54", + "recovered_decimal": "1563.54", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_vintage": "stats_nz_qes_june_2025", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2024-01-01", + "end_date": "2024-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2024-Q1" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2025/2025/25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30/quarterly-employment-survey-june-2025-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2025-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "25053dcbd343320cad72e2896475eaebaf5f5bb0b5965ca788506ca15efcea30", + "source_size_bytes": 135784, + "source_table": "Quarterly Employment Survey June 2025 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2025-quarter/Download-data/quarterly-employment-survey-june-2025-quarter.xlsx", + "vintage": "stats_nz_qes_june_2025" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2024-03-31", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Mar 2024 quarter", + "start_date": "2024-01-01" + }, + "weight": "1", + "sequence": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2024-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2024q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:c07fd9ce3137fa20386bafe0", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:c07fd9ce3137fa20386bafe0", + "decimal_operand": "1586.32", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1586.32", + "recovered_decimal": "1586.32", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2024-04-01", + "end_date": "2024-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2024-Q2" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2024-06-30", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Jun 2024 quarter", + "start_date": "2024-04-01" + }, + "weight": "1", + "sequence": 1 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2024-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2024q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:54a0eeb64049ba4c15e397bf", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:54a0eeb64049ba4c15e397bf", + "decimal_operand": "1595.11", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1595.11", + "recovered_decimal": "1595.11", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2024-07-01", + "end_date": "2024-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2024-Q3" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2024-09-30", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Sep 2024 quarter", + "start_date": "2024-07-01" + }, + "weight": "1", + "sequence": 2 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2024-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2024q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:7fc35ba9682ceb04504f2ef9", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:7fc35ba9682ceb04504f2ef9", + "decimal_operand": "1622.73", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1622.73", + "recovered_decimal": "1622.73", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2024-10-01", + "end_date": "2024-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2024-Q4" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2024-12-31", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Dec 2024 quarter", + "start_date": "2024-10-01" + }, + "weight": "1", + "sequence": 3 + } + ], + "target_schedule": [ + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q2", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q2", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2", + "assertion": "observation" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2", + "decimal_operand": "1700.68", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "1700.68", + "recovered_decimal": "1700.68", + "decimal_values_equal": true + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-04-01", + "end_date": "2026-06-30" + }, + "source_column_id": "SBSZ9A" + }, + "period": { + "type": "quarter", + "value": "2026-Q2" + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/stats_nz/qes_average_weekly_earnings_june_2026/2026/1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97/quarterly-employment-survey-june-2026-quarter.xlsx", + "source_file": "quarterly-employment-survey-june-2026-quarter.xlsx", + "source_name": "stats_nz", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_size_bytes": 124556, + "source_table": "Quarterly Employment Survey June 2026 quarter, Table 7: Average weekly earnings for FTEs, by sector", + "url": "https://www.stats.govt.nz/assets/Uploads/Labour-market-statistics/Labour-market-statistics-June-2026-quarter/Download-data/quarterly-employment-survey-june-2026-quarter.xlsx", + "vintage": "stats_nz_qes_june_2026" + }, + "unit": "nzd_per_week", + "assertion": "observation", + "provenance_class": "survey_aggregate", + "entity_role": "full_time_equivalent_employee", + "period_coverage": { + "basis": "calendar", + "end_date": "2026-06-30", + "notes": "QES reference period is the pay week ending on or before the 20th of the quarter’s middle month (hub-supplied source-period definition); the fact period is the published quarter, not the pay week. Actual, not seasonally adjusted. No annual averaging or period alignment.", + "source_period_label": "Jun 2026 quarter", + "start_date": "2026-04-01" + }, + "status": "observed", + "sequence": 0, + "weight": "1", + "projection_offset": 0 + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q3", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q3", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2026-09-30" + }, + "source_column_id": "SBSZ9A" + }, + "status": "projected", + "sequence": 1, + "weight": "1", + "projection_offset": 1, + "projection_anchor": { + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2" + } + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2026-Q4", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2026q4", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2026-10-01", + "end_date": "2026-12-31" + }, + "source_column_id": "SBSZ9A" + }, + "status": "projected", + "sequence": 2, + "weight": "1", + "projection_offset": 2, + "projection_anchor": { + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2" + } + }, + { + "selector": { + "source_name": "stats_nz", + "source_measure_id": "SBSZ9A", + "source_concept": "stats_nz.qes.average_weekly_earnings.ordinary_time", + "entity_name": "person", + "period_type": "quarter", + "period_value": "2027-Q1", + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "stats_nz.qes.awe.2027q1", + "dimensions": [ + "earnings_component", + "earnings_denominator", + "earnings_sector", + "earnings_series_reference", + "seasonal_adjustment" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "earnings_denominator": "fte", + "earnings_sector": "all_sectors", + "earnings_series_reference": "QEMQ.SBSZ9A", + "seasonal_adjustment": "actual" + }, + "assertion": "observation" + }, + "validation": { + "unit": "nzd_per_week", + "source_sha256": "1af2e7e37f1c108a2656842cf1f519c903e1a982bcdc03ee02d0ad888ebc3a97", + "source_vintage": "stats_nz_qes_june_2026", + "entity_role": "full_time_equivalent_employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", + "period_coverage": { + "start_date": "2027-01-01", + "end_date": "2027-03-31" + }, + "source_column_id": "SBSZ9A" + }, + "status": "projected", + "sequence": 3, + "weight": "1", + "projection_offset": 3, + "projection_anchor": { + "aggregate_fact_key": "ledger.aggregate_fact.v2:91583e3f39d11b8b4b2923d2" + } + } + ], + "subperiods_per_year": 4, + "projection_policy": "observed_preferred_latest_contiguous", + "denominator_mean": { + "numerator": 63677, + "denominator": 40 + }, + "growth": { + "selector": { + "source_name": "treasury", + "source_measure_id": "average_weekly_earnings_annual_percentage_change", + "source_concept": "treasury.prefu26.average_weekly_earnings_annual_percentage_change", + "entity_name": "government", + "period_type": "fiscal_year", + "period_value": 2026, + "geography_level": "country", + "geography_id": "NZ", + "geography_vintage": "current", + "record_set_id": "treasury.prefu26.average_weekly_earnings.fy2026", + "dimensions": [ + "earnings_component", + "publication_status" + ], + "dimension_values": { + "earnings_component": "ordinary_time", + "publication_status": "Forecast" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:aba7d3d54489b068116e6a25", + "assertion": "source_projection", + "source_table": "PREFU 2026 Table 4.1 - Key economic assumptions for fiscal forecasts" + }, + "aggregate_fact_key": "ledger.aggregate_fact.v2:aba7d3d54489b068116e6a25", + "decimal_operand": "2.7", + "numeric_token_proof": { + "numeric_representation": "binary_float_with_lexical_token", + "lexical_token": "2.7", + "recovered_decimal": "2.7", + "decimal_values_equal": true + }, + "validation": { + "unit": "percent", + "source_sha256": "e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9", + "source_vintage": "prefu_2026_09_29", + "entity_role": "forecaster", + "provenance_class": "model_output", + "assertion": "source_projection", + "period_coverage": { + "start_date": "2026-07-01", + "end_date": "2027-06-30" + }, + "source_column_id": "year_ending_30_june_2027" + }, + "period": { + "type": "fiscal_year", + "value": 2026 + }, + "source": { + "extracted_at": "2026-10-22", + "extraction_method": "CellSelectorSpec and SourceRecordSpec resolved from cells", + "method_notes": "Each fact carries source_record_id, source_cell_keys, and source_row_keys when available.", + "raw_r2_bucket": "ledger-raw", + "raw_r2_key": "raw/nz/treasury/treasury-prefu26-economic-assumptions/2026/e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9/prefu26-charts-data.xlsx", + "raw_r2_uri": "r2://ledger-raw/raw/nz/treasury/treasury-prefu26-economic-assumptions/2026/e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9/prefu26-charts-data.xlsx", + "source_file": "prefu26-charts-data.xlsx", + "source_name": "treasury", + "source_sha256": "e2486375dde515d0722f8c3022f481804bb014beb83c81b8bfc99bc9426544d9", + "source_size_bytes": 664670, + "source_table": "PREFU 2026 Table 4.1 - Key economic assumptions for fiscal forecasts", + "url": "https://www.treasury.govt.nz/sites/default/files/2026-09/prefu26-charts-data.xlsx", + "vintage": "prefu_2026_09_29" + }, + "unit": "percent", + "assertion": "source_projection", + "provenance_class": "model_output", + "entity_role": "forecaster", + "period_coverage": { + "basis": "fiscal", + "end_date": "2027-06-30", + "notes": "Publisher Year ending 30 June 2027; Chronicle opening-year fiscal_year:2026. Publisher column status is Forecast. Footnote 3 (Table 4.1!C21:D21): Ordinary time. Footnote 4 (C22:D22): Measures are actuals except for real GDP and nominal GDP. 2026 earnings are therefore actuals despite the Forecast4 column heading; only 2027–2031 earnings are forecasts. The published annual percentage change is retained unchanged; no annual average, level, factor or period alignment is calculated.", + "source_period_label": "Year ending 30 June 2027", + "start_date": "2026-07-01" + }, + "unit_multiplier": "0.01", + "scenario_delta": "0.5", + "sensitivity_provenance": [ + "d1599: symmetric ±0.5 percentage-point Microcosm sensitivity; high is not a Treasury forecast path" + ], + "effective_growth": { + "numerator": 4, + "denominator": 125 + } + }, + "root_bounds": { + "lower": { + "numerator": 100790575349881, + "denominator": 100000000000000 + }, + "upper": { + "numerator": 50395287674941, + "denominator": 50000000000000 + }, + "exact": false, + "decimal_places": 14, + "degree": 4, + "radicand": { + "numerator": 129, + "denominator": 125 + } + }, + "numerator_mean_bounds": { + "lower": { + "numerator": 172095423947015521933465483088234150187075944797, + "denominator": 100000000000000000000000000000000000000000000 + }, + "upper": { + "numerator": 21511927993377262490437471966978882670435826057, + "denominator": 12500000000000000000000000000000000000000000 + } + }, + "unrounded_factor_bounds": { + "lower": { + "numerator": 172095423947015521933465483088234150187075944797, + "denominator": 159192500000000000000000000000000000000000000000 + }, + "upper": { + "numerator": 21511927993377262490437471966978882670435826057, + "denominator": 19899062500000000000000000000000000000000000000 + } + }, + "rounding": { + "digits": 6, + "mode": "nearest_half_even" + }, + "operational_factor": { + "numerator": 1081052, + "denominator": 1000000 + }, + "validity": { + "declared_on": "2026-10-23", + "valid_from": "2026-10-23", + "valid_until": "2026-11-03", + "as_of": "2026-10-23" + }, + "evidence": [ + "Stats NZ QES Table 7; QEMQ.SBSZ9A; ordinary-time FTE, all sectors, actual", + "Treasury PREFU 2026 Table 4.1; year ending 30 June 2027; Chronicle period 2026", + "Declared calendar-2024 money convention (H1), covering flows and asset stocks; the recorded _time_period is a consistency check, not authentication" + ], + "decision_references": [ + "d1952 card; NZ money year build item: donor money is calendar-2024 dollars", + "G8-w hub rulings 5, 24 October 2026, items H1–H3", + "G8-r2 hub rulings 3, 23 October 2026, section 1" + ], + "receipt_sha256": "a26a1e5d24afca6c15b33bc043379c8d96c497a8c30080c89c3c682427d24cbc" + } } ] } diff --git a/packages/microcosm-build/src/microcosm/build/reference_factors.py b/packages/microcosm-build/src/microcosm/build/reference_factors.py index 1a02dade36..d84e56b9e2 100644 --- a/packages/microcosm-build/src/microcosm/build/reference_factors.py +++ b/packages/microcosm-build/src/microcosm/build/reference_factors.py @@ -24,6 +24,11 @@ OPERATION = "projected_period_average_ratio" OPERATION_VERSION = 1 +#: Closed and additive: a reference profile accepts only ``profile_input``, +#: and money normalization accepts only ``money_normalization_input``. +FACTOR_PURPOSES = frozenset( + {"profile_input", "diagnostic", "money_normalization_input"} +) # Exact powers in projected receipts grow with both schedule size and root # precision. Keep ordinary schedules within the stock integer serialization # budget, including when rounding needs a finer root enclosure. @@ -183,7 +188,7 @@ def _declaration(declaration: object) -> dict: raise ValueError("Unknown factor operation or operation version.") if not isinstance(row["id"], str) or not row["id"]: raise ValueError("Factor id must be nonempty text.") - if row["purpose"] not in {"profile_input", "diagnostic"}: + if row["purpose"] not in FACTOR_PURPOSES: raise ValueError("Unknown factor purpose.") for field in ("source_period", "target_period"): label = row[field] @@ -437,6 +442,30 @@ def _operand( } +def resolve_feed_operand( + slot: Mapping, facts: Iterable, *, assertion_policy: str = "observed_only" +) -> tuple[Fraction, dict]: + """Resolve one declared operand exactly once, as factor operands resolve. + + The slot is ``{"selector", "validation"}`` in the factor vocabulary. The + exact value comes from the authenticated lexical token, never a float. + """ + row = _closed(slot, {"selector", "validation"}, "feed operand") + selector = _mapping(row.get("selector"), "feed operand selector") + if not selector or any(not selector_field_is_supported(key) for key in selector): + raise ValueError( + "Unknown or empty feed operand selector; use the closed selector vocabulary." + ) + if assertion_policy not in ALLOWED_ASSERTION_POLICIES: + raise ValueError("Unknown feed operand assertion policy.") + _validation(row.get("validation")) + return _operand( + {"selector": selector, "validation": row["validation"]}, + tuple(facts), + assertion_policy=assertion_policy, + ) + + def resolve_reference_factor( declaration: Mapping, facts: Iterable, feed_identity: Mapping ) -> tuple[dict, dict]: diff --git a/packages/microcosm-build/src/microcosm/build/transport/compose.py b/packages/microcosm-build/src/microcosm/build/transport/compose.py index 4c7a91417c..ea7129b29d 100644 --- a/packages/microcosm-build/src/microcosm/build/transport/compose.py +++ b/packages/microcosm-build/src/microcosm/build/transport/compose.py @@ -49,7 +49,10 @@ shapes, the receipt kernel's data-independent contract constraints, reference activation rows, required age/pin presence and null scenario knobs. Refusals name every enumerated gap (:func:`validate_transport_activation`). Optional -null JSON fields remain allowed; no proposed value is chosen. +null JSON fields remain allowed; no proposed value is chosen. A spec that +declares the opt-in ``money_normalization`` resource also gets its consumed +money columns checked for exactly one declared producer, and composition +refuses a second money producer in any population lineage. """ from __future__ import annotations @@ -72,12 +75,18 @@ CHRONICLE_FEED_RESOURCE, chronicle_feed_identity, ) +from microcosm.build.money_normalization import ( + money_column_rosters, + money_concept_columns, + validate_money_normalization, +) from microcosm.build.reference_profiles import resolve_reference_profiles from microcosm.calibrate.ordered_kernels import OUTPUT_TYPES from microcosm.graph import ( ArtifactInput, ArtifactOutput, ArtifactType, + CompiledGraph, Graph, Node, Owned, @@ -92,10 +101,12 @@ from .artifact_types import KERNEL_OUTPUTS from .column_kernels import validate_receipt_contract +from .money_kernels import MONEY_NORMALIZE_REF from .target_kernels import TARGET_COMPILATION_KERNELS __all__ = [ "GRAPH_RESOURCE", + "MONEY_NORMALIZATION_RESOURCE", "RULES_ROUTER", "SKELETON_KERNELS", "TransportGraphConfig", @@ -126,6 +137,7 @@ "transport.boundary@1", "transport.currency@1", "transport.quantile_map@1", + MONEY_NORMALIZE_REF, "transport.unit_attributes@1", "concepts.encode@1", "concepts.encode_groups@1", @@ -146,6 +158,16 @@ "transport.package@1", } ) +#: The opt-in spec resource that declares exact donor money normalization. +MONEY_NORMALIZATION_RESOURCE = "money_normalization" +#: The only kernels that may produce donor money after CREATE once a spec +#: declares money normalization. +_MONEY_PRODUCERS = frozenset({MONEY_NORMALIZE_REF, "transport.quantile_map@1"}) +#: Conversion changes the basis of an existing donor column; derived money +#: outputs and cross-population evidence artifacts do not change that basis. +_MONEY_CONVERSIONS = frozenset({MONEY_NORMALIZE_REF, "transport.currency@1"}) +#: Kernels outside target compilation that replay factors against the feed. +_FEED_REPLAY_KERNELS = frozenset({MONEY_NORMALIZE_REF}) _DOCUMENT_FIELDS = frozenset( { "schema_version", @@ -341,7 +363,7 @@ def _target_feed_params( """Bind the declared feed to every target compiler, including preparation.""" result = dict(params) - if kernel in TARGET_COMPILATION_KERNELS and _has_resource( + if kernel in TARGET_COMPILATION_KERNELS | _FEED_REPLAY_KERNELS and _has_resource( spec, CHRONICLE_FEED_RESOURCE ): pin = canonical_json(transport_resource(spec, CHRONICLE_FEED_RESOURCE)).decode( @@ -561,6 +583,269 @@ def _reference_activation_gaps(document: Mapping, resource: str) -> Iterator[str yield f"placeholder reference {resource}.{reference.get('name', '?')}" +def _declared_rows(spec: Mapping[str, object]) -> list[Mapping]: + """Skeleton rows plus the entitlement rows the factory instantiates.""" + rows = [] + if _has_resource(spec, GRAPH_RESOURCE): + nodes = transport_resource(spec, GRAPH_RESOURCE).get("nodes", ()) + rows.extend( + row + for row in _sequence(nodes, "transport nodes") + if isinstance(row, Mapping) + ) + if _has_resource(spec, "entitlement_graph") and _has_resource(spec, "scenarios"): + from .entitlement import _rows + + rows.extend(row for row, _ in _rows(spec)[0]) + return rows + + +def _owned_columns(row: Mapping, *, rewrite: bool | None = None) -> set: + return { + (output.get("entity"), output.get("column")) + for output in row.get("outputs", ()) + if isinstance(output, Mapping) + and (rewrite is None or bool(output.get("rewrite")) is rewrite) + } + + +def _derived_money_owner(key, donor_columns, kernel, *, rewrite: bool) -> bool: + """Distinguish newly derived money from the declaration's donor columns. + + A new engine output may be a money concept (for example a destination + pension), but it neither rewrites donor money nor supplies its basis. + A declared donor route always retains its stricter producer contract. + """ + return ( + key not in donor_columns + and kernel not in _MONEY_PRODUCERS | _MONEY_CONVERSIONS + and not rewrite + ) + + +def _money_producer_gaps(spec: Mapping[str, object]) -> list[str]: + """Name every consumed money column without exactly one declared producer. + + Opt-in: a spec without the money-normalization resource returns no gap + and reads nothing. Otherwise every donor money concept a node reads + after CREATE needs exactly one producer in its population lineage (its + own version and each base version): ``money.normalize@1`` for a + normalized column, ``transport.quantile_map@1`` for a quantile-mapped + one. The concept's default transport rule is not consulted. A consumed + pending or quantile-mapped column without its producer names what it + awaits. New destination money outputs are classified separately. + """ + if not _has_resource(spec, MONEY_NORMALIZATION_RESOURCE): + return [] + try: + declaration = validate_money_normalization( + _raw_transport_resource(spec, MONEY_NORMALIZATION_RESOURCE) + ) + except (ValueError, TypeError) as error: + return [f"{MONEY_NORMALIZATION_RESOURCE}.json: {error}"] + rows = _declared_rows(spec) + structural = { + row["id"]: row + for row in rows + if row.get("structural", "none") != "none" and isinstance(row.get("id"), str) + } + default = next(iter(structural)) if len(structural) == 1 else None + + def lineage(version: object) -> list[str]: + chain: list[str] = [] + while isinstance(version, str) and version in structural: + if version in chain: + break + chain.append(version) + version = structural[version].get("base") + return chain + + owners: dict[tuple, list[Mapping]] = {} + for row in rows: + if row.get("structural", "none") == "none": + version = row.get("population") or default + for entity, column in _owned_columns(row): + owners.setdefault((version, entity, column), []).append(row) + money = money_concept_columns() + rosters = money_column_rosters(declaration) + found: dict[str, set[str]] = {} + for row in rows: + reads = ( + row.get("base") + if row.get("structural", "none") != "none" + else row.get("population") or default + ) + donor_columns = set(rosters) + donor_columns.update( + key + for version in lineage(reads) + if structural[version].get("structural") == "create" + for key in _owned_columns(structural[version]) + ) + rewritten = _owned_columns(row, rewrite=True) + for item in row.get("inputs", ()): + if not isinstance(item, Mapping): + continue + for column in item.get("columns", ()): + key = (item.get("entity"), column) + if key not in money or key in rewritten: + continue + producers = [ + producer + for version in lineage(reads) + for producer in owners.get((version, *key), ()) + ] + route, awaits = rosters.get(key, (None, ())) + name = f"money column {key[0]}.{key[1]}" + if not producers: + if route == "pending": + message = f"{name} awaits {', '.join(awaits)}" + elif route == "quantile_mapped": + message = f"{name} awaits quantile map <- {', '.join(awaits)}" + elif route == "normalized": + message = f"{name} has no {MONEY_NORMALIZE_REF} producer" + else: + message = ( + f"{name} is not declared in " + f"{MONEY_NORMALIZATION_RESOURCE}.json" + ) + elif len(producers) > 1: + message = ( + f"{name} has {len(producers)} producers in one lineage " + f"{sorted(str(item.get('id')) for item in producers)}" + ) + else: + (producer,) = producers + kernel = producer.get("kernel") + if _derived_money_owner( + key, + donor_columns, + kernel, + rewrite=key in _owned_columns(producer, rewrite=True), + ): + continue + expected = { + "normalized": MONEY_NORMALIZE_REF, + "quantile_mapped": "transport.quantile_map@1", + }.get(route) + if kernel not in _MONEY_PRODUCERS: + message = ( + f"{name} is produced by {kernel} node " + f"{producer.get('id')!r}; only {MONEY_NORMALIZE_REF} or " + "transport.quantile_map@1 may produce it after CREATE" + ) + elif kernel != expected: + message = ( + f"{name} is declared {route} but produced by {kernel} " + f"node {producer.get('id')!r}" + ) + else: + continue + found.setdefault(message, set()).add(str(row.get("id"))) + return [ + f"{message} (read by {sorted(readers)})" + for message, readers in sorted(found.items()) + ] + + +def _check_single_money_normalization( + spec: Mapping[str, object], graph: Graph, compiled: CompiledGraph +) -> None: + """Track donor money ownership and currency basis through base versions. + + Opt-in like :func:`_money_producer_gaps`. A FILTER carries every column + of its base version and its members. Artifact predecessors cross versions + without changing ownership, so they never enter this structural chain. + Newly derived money is classified separately from declared donor money. + """ + if not _has_resource(spec, MONEY_NORMALIZATION_RESOURCE): + return + declaration = validate_money_normalization( + _raw_transport_resource(spec, MONEY_NORMALIZATION_RESOURCE) + ) + rosters = money_column_rosters(declaration) + money = money_concept_columns() + expected = { + MONEY_NORMALIZE_REF: "normalized", + "transport.quantile_map@1": "quantile_mapped", + } + by_id = {node.id: node for node in graph.nodes} + for (_, entity, column), owner in compiled.owners.items(): + node = by_id[owner] + if node.kernel in expected and (entity, column) in money: + route = rosters.get((entity, column), (None, ()))[0] + if route != expected[node.kernel]: + raise ValueError( + f"Money column {entity}.{column} is declared {route} but " + f"{node.kernel} node {owner!r} produces it." + ) + for version in sorted(set(compiled.versions.values())): + chain, current = [], version + while current is not None and current not in chain: + chain.append(current) + current = by_id[current].base + donor_columns = set(rosters) + donor_columns.update( + (entity, column) + for (item, entity, column), owner in compiled.owners.items() + if item in chain and by_id[owner].structural is StructuralDelta.CREATE + ) + normalizers = sorted( + node.id + for node in graph.nodes + if node.kernel == MONEY_NORMALIZE_REF + and compiled.versions[node.id] in chain + ) + if len(normalizers) > 1: + raise ValueError( + f"Money is normalized more than once in the lineage of {version!r}: " + f"{normalizers}." + ) + for entity, column in money: + producers = sorted( + owner + for item in chain + if (owner := compiled.owners.get((item, entity, column))) is not None + and by_id[owner].structural is StructuralDelta.NONE + ) + conversions = [ + owner + for owner in producers + if by_id[owner].kernel in _MONEY_CONVERSIONS + ] + if len(conversions) > 1: + raise ValueError( + f"Money is normalized more than once in the lineage of {version!r} " + f"for {entity}.{column}: {conversions}." + ) + if producers and all( + _derived_money_owner( + (entity, column), + donor_columns, + by_id[owner].kernel, + rewrite=any( + item.entity == entity and item.column == column and item.rewrite + for item in by_id[owner].outputs + ), + ) + for owner in producers + ): + continue + for owner in producers: + kernel = by_id[owner].kernel + route = rosters.get((entity, column), (None, ()))[0] + if kernel not in expected or route != expected[kernel]: + raise ValueError( + f"Money column {entity}.{column} is declared {route} but " + f"{kernel} node {owner!r} produces it." + ) + if len(producers) > 1: + raise ValueError( + f"Money column {entity}.{column} has {len(producers)} producers " + f"in the lineage of {version!r}: {producers}." + ) + + def validate_transport_activation(spec: Mapping[str, object]) -> None: """Refuse enumerated activation gaps before registry/CREATE/source reads. @@ -686,6 +971,7 @@ def validate_transport_activation(spec: Mapping[str, object]) -> None: missing.append(f"scenario {scenario.get('id', '?')} {path}") if not _has_resource(spec, GRAPH_RESOURCE): missing.append(f"{GRAPH_RESOURCE}.json (the skeleton declaration)") + missing.extend(_money_producer_gaps(spec)) if missing: raise ValueError("Transport spec is not activated: " + "; ".join(missing) + ".") @@ -1399,6 +1685,8 @@ def compose_transport_graph( ) compiled = compile_graph(skeleton) validate_transport_calibration_ancestry(spec, graph=skeleton, config=config) + # Money ownership is checked after each graph pass of the ancestry guard. + _check_single_money_normalization(spec, skeleton, compiled) endpoints = transport_endpoints(spec) named = {endpoints["geography"], endpoints["calibration"], *endpoints["terminal"]} if not named <= set(compiled.order): @@ -1487,6 +1775,8 @@ def compose_transport_graph( extended = compile_graph(graph) _check_extension_checkpoints(spec, config, graph, extended) validate_transport_calibration_ancestry(spec, graph=graph, config=config) + # Money ownership is checked after each graph pass of the ancestry guard. + _check_single_money_normalization(spec, graph, extended) for node in skeleton.nodes: expected = ( set(compiled.predecessors[node.id]) diff --git a/packages/microcosm-build/src/microcosm/build/transport/money_kernels.py b/packages/microcosm-build/src/microcosm/build/transport/money_kernels.py new file mode 100644 index 0000000000..c42e0093b1 --- /dev/null +++ b/packages/microcosm-build/src/microcosm/build/transport/money_kernels.py @@ -0,0 +1,295 @@ +"""Exact donor money normalization as a graph kernel. + +``money.normalize@1`` rewrites every column a spec's money-normalization +declaration lists, once, and adds each column's integer minor-unit companion. +Its parameters are the declaration (canonical JSON) and the factor context +(the country's declared factor document), each with its resource digest, plus +the scenario's profile selection. The node reads exactly one source, the +authenticated Chronicle feed: every selected factor is replayed against it +with :func:`~microcosm.build.reference_factors.replay_reference_factor`, as +target compilation does, so a self-consistent receipt alone never +authenticates a factor. A feed-selected exchange rate is replayed from the +same facts. + +CREATE records the donor's model period in the frame metadata +(``donor_time_period``); the kernel refuses a frame whose recorded period is +absent, null or different from the declaration's. This checks consistency +with the declared money basis; it does not authenticate that basis. +""" + +from __future__ import annotations + +import pandas as pd + +import microcosm.build.chronicle_epoch as chronicle_epoch_module +import microcosm.build.ledger_artifact as ledger_artifact_module +import microcosm.build.ledger_targets as ledger_targets_module +import microcosm.build.money_normalization as money_normalization_module +import microcosm.build.reference_factors as reference_factors_module +import microcosm.frame.concepts as concepts_module +from microcosm.build.ledger_artifact import ( + chronicle_feed_identity, + load_ledger_consumer_artifact, + validate_chronicle_feed_pin, +) +from microcosm.build.money_normalization import ( + money_factor_inputs, + normalize_money_tables, + normalized_column_families, + resolve_money_rate, + selected_money_factors, + validate_money_normalization, +) +from microcosm.build.reference_factors import ( + replay_reference_factor, + validate_factor_context, +) +from microcosm.graph import ( + ROWS_ALL, + Capabilities, + Determinism, + KernelBase, + KernelContext, + KernelRegistry, + KernelResult, + Numeric, + SeedSource, + source_hash, +) + +from . import graph_inputs +from .graph_inputs import ( + canonical_document_param, + require_params, + sha256_param, + single_source, +) + +__all__ = [ + "MONEY_NORMALIZE", + "MONEY_NORMALIZE_REF", + "MoneyNormalizeKernel", + "register_money_kernels", +] + +MONEY_NORMALIZE_REF = "money.normalize@1" +#: The frame-metadata key CREATE writes from the donor's ``_time_period``. +DONOR_TIME_PERIOD = "donor_time_period" + + +def _recorded_period(context: KernelContext, declaration) -> int: + if DONOR_TIME_PERIOD not in context.frame_metadata: + raise ValueError( + f"{MONEY_NORMALIZE_REF} needs the frame's recorded {DONOR_TIME_PERIOD}; " + "CREATE records it from the donor." + ) + recorded = context.frame_metadata[DONOR_TIME_PERIOD] + declared = declaration["donor_money_period"] + if recorded is None: + raise ValueError( + f"The donor records no money period, but the declaration names " + f"{declared['type']} {declared['value']}." + ) + if type(recorded) is not int or recorded != declared["value"]: + raise ValueError( + f"The donor's recorded money period {recorded!r} differs from the " + f"declared {declared['type']} {declared['value']}." + ) + return recorded + + +def _authenticated_facts(context: KernelContext, entries) -> tuple[tuple, dict]: + """Load the one facts source and authenticate it as target compilation does.""" + source = single_source(context, MONEY_NORMALIZE_REF) + feed_pin = ( + canonical_document_param(context, MONEY_NORMALIZE_REF, "chronicle_feed_pin") + if "chronicle_feed_pin" in context.params + else None + ) + artifact = load_ledger_consumer_artifact( + context.sources[source], feed_pin=feed_pin, lexical_floats=True + ) + identities = [] + for entry in entries: + identity = chronicle_feed_identity( + feed_pin if feed_pin is not None else entry["declaration"]["feed_identity"] + ) + if feed_pin is None: + validate_chronicle_feed_pin(artifact, identity) + if (artifact.manifest or {}).get("source_commit") != identity[ + "source_commit" + ]: + raise ValueError( + "Factor source commit cannot be authenticated without a feed pin." + ) + replay_reference_factor( + entry["declaration"], + artifact.facts, + identity, + entry["operational_factor"], + entry["receipt"], + ) + identities.append(identity) + if feed_pin is not None: + identity = chronicle_feed_identity(feed_pin) + elif identities: + identity = identities[0] + else: + identity = None + return artifact.facts, { + "identity": identity, + "facts_sha256": artifact.facts_sha256, + "manifest_sha256": artifact.manifest_sha256, + } + + +class MoneyNormalizeKernel(KernelBase): + """``money.normalize@1``: declared exact currency and period normalization.""" + + ref = MONEY_NORMALIZE_REF + capabilities = Capabilities( + Determinism.DETERMINISTIC, + numeric=Numeric.BITWISE, + seed_source=SeedSource.NONE, + dependencies=("numpy", "pandas"), + ) + _required = frozenset( + { + "declaration", + "declaration_sha256", + "factor_context", + "factor_context_sha256", + "profile_selection", + } + ) + + def implementation_hash(self) -> str: + return source_hash( + type(self), + graph_inputs, + money_normalization_module, + reference_factors_module, + ledger_artifact_module, + ledger_targets_module, + chronicle_epoch_module, + concepts_module, + dependencies=self.capabilities.dependencies, + ) + + def run(self, context: KernelContext) -> KernelResult: + require_params( + context, + self.ref, + required=self._required, + optional=frozenset({"chronicle_feed_pin"}), + ) + if context.artifacts or context.node.artifact_outputs: + raise ValueError(f"{self.ref} reads and writes no artifacts.") + if any(item.rows != ROWS_ALL for item in context.node.inputs) or any( + item.rows != ROWS_ALL for item in context.node.outputs + ): + raise ValueError(f"{self.ref} requires whole-row slices and outputs.") + for name in ("declaration_sha256", "factor_context_sha256"): + sha256_param(context, self.ref, name) + declaration = validate_money_normalization( + canonical_document_param(context, self.ref, "declaration") + ) + period = _recorded_period(context, declaration) + profile, chosen = selected_money_factors( + declaration, + canonical_document_param(context, self.ref, "profile_selection"), + ) + families = normalized_column_families(declaration) + suffix = declaration["cents_suffix"] + expected_outputs = { + (entity, column, "float64", True) for entity, column in families + } | { + (entity, f"{column}{suffix}", "int64", False) for entity, column in families + } + declared_outputs = { + (item.entity, item.column, item.dtype, item.rewrite) + for item in context.node.outputs + } + if declared_outputs != expected_outputs: + raise ValueError( + f"{self.ref} must rewrite exactly the declared money columns as " + "float64 and add each int64 companion, and own nothing else." + ) + sliced = { + (item.entity, column) + for item in context.node.inputs + for column in item.columns + } + if sliced != set(families): + raise ValueError( + f"{self.ref} slices exactly the declared money columns it rewrites." + ) + factor_ids = sorted({value for value in chosen.values() if value is not None}) + entries = validate_factor_context( + canonical_document_param(context, self.ref, "factor_context"), factor_ids + ) + inputs = { + factor_id: money_factor_inputs(entries[factor_id], declaration) + for factor_id in factor_ids + } + facts, feed = _authenticated_facts( + context, [entries[factor_id] for factor_id in factor_ids] + ) + rate, rate_evidence = resolve_money_rate(declaration, facts, feed["identity"]) + factors = { + family: ( + { + "identity": True, + "reason": declaration["index_family_factors"][family]["reason"], + } + if factor_id is None + else inputs[factor_id] + ) + for family, factor_id in chosen.items() + } + arrays, columns = normalize_money_tables( + context.tables, + declaration, + rate=rate, + rate_text=rate_evidence["text"], + factors=factors, + ) + result = {} + for (entity, column), values in arrays.items(): + ids = context.tables[entity][f"{entity}_id"] + result[entity, column] = pd.Series( + values, + index=pd.Index(ids.to_numpy(copy=True), name=f"{entity}_id"), + name=column, + dtype=values.dtype, + ) + return KernelResult( + columns=result, + receipt={ + "declaration_sha256": context.params["declaration_sha256"], + "factor_context_sha256": context.params["factor_context_sha256"], + "from_currency": declaration["from_currency"], + "to_currency": declaration["to_currency"], + "quote": declaration["quote"], + "rounding": dict(declaration["rounding"]), + "rate": rate_evidence, + "donor_money_period": dict(declaration["donor_money_period"]), + "recorded_donor_time_period": period, + "profile": profile, + "factors": {family: factors[family] for family in sorted(factors)}, + "feed": { + "facts_sha256": feed["facts_sha256"], + "manifest_sha256": feed["manifest_sha256"], + }, + "columns": columns, + }, + ) + + +MONEY_NORMALIZE = MoneyNormalizeKernel() + + +def register_money_kernels(registry: KernelRegistry) -> KernelRegistry: + """Register the money kernel explicitly and idempotently.""" + registry.register(MONEY_NORMALIZE) + return registry diff --git a/packages/microcosm-build/src/microcosm/build/transport/population_kernels.py b/packages/microcosm-build/src/microcosm/build/transport/population_kernels.py index ccbc6a609b..ed6ba2368f 100644 --- a/packages/microcosm-build/src/microcosm/build/transport/population_kernels.py +++ b/packages/microcosm-build/src/microcosm/build/transport/population_kernels.py @@ -273,6 +273,7 @@ def run(self, context: KernelContext) -> KernelResult: "donor_country": donor.donor_country, "country": country, "donor_currency": donor.currency, + "donor_time_period": donor.time_period, }, ) return KernelResult( @@ -281,6 +282,7 @@ def run(self, context: KernelContext) -> KernelResult: "content_basis": donor.content_basis.value, "donor_country": donor.donor_country, "country": country, + "donor_time_period": donor.time_period, "dropped_columns": { entity: list(names) for entity, names in dropped.items() }, diff --git a/packages/microcosm-build/src/microcosm/build/transport/registry.py b/packages/microcosm-build/src/microcosm/build/transport/registry.py index 57a9d74497..269dcb3a29 100644 --- a/packages/microcosm-build/src/microcosm/build/transport/registry.py +++ b/packages/microcosm-build/src/microcosm/build/transport/registry.py @@ -46,6 +46,7 @@ from .gate_bindings import TRANSPORT_GATE_REGISTRY from .gate_kernels import register_gate_kernel from .geography_kernels import register_geography_kernels +from .money_kernels import register_money_kernels from .population_kernels import register_population_kernels from .reporting_kernels import register_reporting_kernels from .takeup_kernels import register_takeup_kernels @@ -388,7 +389,7 @@ def build_transport_registry( def register_transport_population_kernels(registry: KernelRegistry) -> KernelRegistry: - """Install the ten population kernels without import-time side effects. + """Install the eleven population kernels without import-time side effects. Source codecs are separately installed with ``register_transport_codecs``; target, solver and terminal kernels keep their own registration functions. @@ -397,4 +398,5 @@ def register_transport_population_kernels(registry: KernelRegistry) -> KernelReg register_population_kernels(registry) register_column_kernels(registry) register_geography_kernels(registry) + register_money_kernels(registry) return registry diff --git a/packages/microcosm-build/tests/engine_free/shared/test_ledger_artifact.py b/packages/microcosm-build/tests/engine_free/shared/test_ledger_artifact.py index 7dc217711d..2c59d73e20 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_ledger_artifact.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_ledger_artifact.py @@ -148,6 +148,7 @@ def test_lexical_float_opt_in_is_limited_to_factor_consumers(): continue opt_ins.add(path.relative_to(repository).as_posix()) assert opt_ins == { + "packages/microcosm-build/src/microcosm/build/transport/money_kernels.py", "packages/microcosm-build/src/microcosm/build/transport/target_kernels.py", "tools/generate_nz_reference_aging.py", } diff --git a/packages/microcosm-build/tests/engine_free/shared/test_money_normalization.py b/packages/microcosm-build/tests/engine_free/shared/test_money_normalization.py new file mode 100644 index 0000000000..f8d3acd787 --- /dev/null +++ b/packages/microcosm-build/tests/engine_free/shared/test_money_normalization.py @@ -0,0 +1,791 @@ +"""Exact donor money normalization: declaration, arithmetic, receipts, factors. + +The property tests compare against an independent oracle built only from +``fractions.Fraction`` and the built-in ``round`` (ties to even); it never +calls a production helper. +""" + +from __future__ import annotations + +import copy +import math +from decimal import localcontext +from fractions import Fraction + +import numpy as np +import pandas as pd +import pytest +from hypothesis import HealthCheck, example, given, settings +from hypothesis import strategies as st + +from microcosm.build.money_normalization import ( + MONEY_FACTOR_PURPOSE, + check_money_factor, + money_column_rosters, + money_factor_inputs, + normalize_money_tables, + normalized_column_families, + resolve_money_rate, + selected_money_factors, + validate_money_normalization, +) +from microcosm.build.reference_factors import FACTOR_PURPOSES +from test_support.microcosm_build.money_normalization import ( + DIMENSION, + NORMALIZED, + factor_id, + money_factor_context, + oracle_factor, + rate_feed_form, + toy_declaration, + write_money_feed, +) + +PROPERTY = settings( + max_examples=200, + deadline=None, + derandomize=True, + database=None, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +_FIELDS = ( + "schema_version", + "from_currency", + "to_currency", + "rate", + "quote", + "rounding", + "donor_money_period", + "columns", + "index_family_factors", + "factor_profile", + "cents_suffix", + "quantile_mapped", + "pending", +) + + +def _oracle(value: float, rate: Fraction, quote: str, factor: Fraction, digits=2): + """Two roundings from first principles: currency, then factor.""" + x = Fraction(repr(float(value))) + scale = 10**digits + converted = x * rate * scale if quote == "to_per_from" else x * scale / rate + first = round(converted) + return first, round(first * factor), round(converted * factor) + + +def _identity_factor(): + return {"identity": True, "reason": "test"} + + +def _factor(numerator: int, digits: int = 6, name: str = "f") -> dict: + return { + "factor_id": name, + "numerator": numerator, + "digits": digits, + "declaration_sha256": "d" * 64, + "receipt_sha256": "e" * 64, + } + + +def _one_column_declaration(column="rental_income", entity="person", **options): + """The toy declaration with one normalized column and the rest pending.""" + declaration = toy_declaration(**options) + validated = validate_money_normalization(copy.deepcopy(declaration)) + family = normalized_column_families(validated)[entity, column] + others = { + key: value + for key, value in money_column_rosters(validated).items() + if value[0] == "normalized" and key != (entity, column) + } + declaration["columns"] = {entity: [column]} + declaration["index_family_factors"] = { + family: declaration["index_family_factors"][family] + } + pending = {} + for other_entity, other_column in others: + pending.setdefault(other_entity, {})[other_column] = { + "awaits": ["test"], + "reason": "test", + } + declaration["pending"] = pending + return validate_money_normalization(declaration), family + + +def _normalize(values, *, rate="1.25", quote="to_per_from", factor=None, column=None): + column = column or "rental_income" + declaration, family = _one_column_declaration(column, rate=rate, quote=quote) + tables = { + "person": pd.DataFrame( + {"person_id": np.arange(len(values)), column: np.asarray(values, float)} + ) + } + arrays, receipts = normalize_money_tables( + tables, + declaration, + rate=Fraction(rate), + rate_text=rate, + factors={family: factor or _identity_factor()}, + ) + return arrays, receipts, declaration, tables + + +# --------------------------------------------------------------------------- +# Declaration validation +# --------------------------------------------------------------------------- + + +def test_toy_declaration_validates_and_rosters_partition_every_money_concept(): + declaration = validate_money_normalization(toy_declaration()) + rosters = money_column_rosters(declaration) + assert {key for key, (route, _) in rosters.items() if route == "normalized"} == { + (entity, column) for entity, names in NORMALIZED.items() for column in names + } + assert {key for key, (route, _) in rosters.items() if route != "normalized"} == { + ("person", "employment_income"), + ("person", "liquid_financial_assets"), + ("household", "rent"), + } + assert ("person", "public_pension_income") not in rosters + assert set(normalized_column_families(declaration).values()) == { + "mixed_income", + "capital_income", + "pensions", + "owner_housing_costs", + } + + +@pytest.mark.parametrize("field", _FIELDS) +def test_every_field_is_required(field): + declaration = toy_declaration() + declaration.pop(field) + with pytest.raises(ValueError, match=f"missing fields \\['{field}'\\]"): + validate_money_normalization(declaration) + + +def test_unknown_fields_refuse_and_descriptive_text_rides_along(): + declaration = toy_declaration() + declaration["fallback_rate"] = "1" + with pytest.raises(ValueError, match="unknown fields"): + validate_money_normalization(declaration) + declaration.pop("fallback_rate") + declaration["notes"] = "Reviewed prose." + assert validate_money_normalization(declaration)["notes"] == "Reviewed prose." + + +@pytest.mark.parametrize("value", [1.25, 1, None, True]) +def test_a_rate_given_as_a_json_number_refuses(value): + declaration = toy_declaration() + declaration["rate"]["text"] = value + with pytest.raises(ValueError, match="decimal text, never a JSON number"): + validate_money_normalization(declaration) + + +@pytest.mark.parametrize("text", ["1.2e3", "-1.25", "0", "0.000", " 1.25", "1,25", ""]) +def test_rate_text_must_be_plain_positive_decimal(text): + declaration = toy_declaration() + declaration["rate"]["text"] = text + with pytest.raises(ValueError, match="rate text"): + validate_money_normalization(declaration) + + +@pytest.mark.parametrize( + "edit", + [ + lambda rate: rate.pop("citation"), + lambda rate: rate["citation"].pop("url"), + lambda rate: rate.update(citation="IRS"), + lambda rate: rate.update(form="literal"), + lambda rate: rate.update(extra="x"), + ], +) +def test_decimal_rate_needs_its_citation_and_closed_form(edit): + declaration = toy_declaration() + edit(declaration["rate"]) + with pytest.raises(ValueError, match="rate"): + validate_money_normalization(declaration) + + +@pytest.mark.parametrize( + ("path", "value", "message"), + [ + (("quote",), "multiply", "quote"), + (("quote",), None, "quote"), + (("rounding", "mode"), "half_up", "half_even"), + (("rounding", "digits"), 7, "digits"), + (("rounding", "digits"), 2.0, "digits"), + (("donor_money_period", "type"), "fiscal_year", "period type"), + (("donor_money_period", "value"), "2024", "year"), + (("from_currency",), "usd", "ISO 4217"), + (("to_currency",), "USD", "distinct currencies"), + (("cents_suffix",), "cents", "cents_suffix"), + (("schema_version",), 2, "schema_version"), + ], +) +def test_closed_values_refuse(path, value, message): + declaration = toy_declaration() + target = declaration + for key in path[:-1]: + target = target[key] + target[path[-1]] = value + with pytest.raises(ValueError, match=message): + validate_money_normalization(declaration) + + +def test_rounding_and_period_objects_are_closed(): + declaration = toy_declaration() + declaration["rounding"]["places"] = 2 + with pytest.raises(ValueError, match="rounding has unknown"): + validate_money_normalization(declaration) + declaration = toy_declaration() + declaration["donor_money_period"].pop("value") + with pytest.raises(ValueError, match="donor_money_period is missing"): + validate_money_normalization(declaration) + + +@pytest.mark.parametrize("field", ["type", "value", "basis", "evidence"]) +def test_every_donor_period_field_is_required(field): + declaration = toy_declaration() + declaration["donor_money_period"].pop(field) + with pytest.raises(ValueError, match=f"missing fields \\['{field}'\\]"): + validate_money_normalization(declaration) + + +@pytest.mark.parametrize("basis", ["declared_convention", "authenticated"]) +def test_donor_period_basis_is_explicit_with_evidence(basis): + declaration = toy_declaration() + declaration["donor_money_period"]["basis"] = basis + validated = validate_money_normalization(declaration) + assert validated["donor_money_period"] == declaration["donor_money_period"] + + +@pytest.mark.parametrize( + ("field", "value", "message"), + [ + ("basis", "model_period", "period basis"), + ("basis", None, "period basis"), + ("evidence", [], "evidence"), + ("evidence", "not a citation list", "evidence"), + ("evidence", [""], "evidence"), + ("evidence", [None], "evidence"), + ], +) +def test_donor_period_refuses_an_unknown_basis_or_missing_evidence( + field, value, message +): + declaration = toy_declaration() + declaration["donor_money_period"][field] = value + with pytest.raises(ValueError, match=message): + validate_money_normalization(declaration) + + +def test_each_column_appears_in_exactly_one_roster(): + declaration = toy_declaration() + declaration["columns"]["person"].append("employment_income") + with pytest.raises(ValueError, match="both normalized and quantile_mapped"): + validate_money_normalization(declaration) + declaration = toy_declaration() + declaration["pending"] = { + "household": {"rent": {"awaits": ["x"], "reason": "x"}}, + } + with pytest.raises(ValueError, match="both quantile_mapped and pending"): + validate_money_normalization(declaration) + + +def test_every_transported_money_concept_must_be_declared(): + declaration = toy_declaration() + declaration["columns"]["household"].remove("property_tax") + with pytest.raises(ValueError, match=r"unclassified: \['household.property_tax'\]"): + validate_money_normalization(declaration) + + +def test_rosters_name_money_concepts_and_their_awaited_references(): + declaration = toy_declaration() + declaration["columns"]["person"].append("age") + with pytest.raises(ValueError, match="not a money concept"): + validate_money_normalization(declaration) + for awaits in ([], [""], "toy bands", ["x", "x"]): + declaration = toy_declaration() + declaration["quantile_mapped"]["household"]["rent"]["awaits"] = awaits + with pytest.raises(ValueError, match="awaits"): + validate_money_normalization(declaration) + + +def test_a_quantile_mapped_drop_default_column_is_admitted(): + # Belgium condition d: the declaration does not consult transport rules. + declaration = toy_declaration() + declaration["quantile_mapped"]["person"]["public_pension_income"] = { + "awaits": ["toy pension bands"], + "reason": "A country may quantile-map a column whose default is drop.", + } + rosters = money_column_rosters(validate_money_normalization(declaration)) + assert rosters["person", "public_pension_income"][0] == "quantile_mapped" + + +def test_an_unmapped_family_refuses_rather_than_defaulting_to_one(): + declaration = toy_declaration() + declaration["index_family_factors"].pop("pensions") + with pytest.raises(ValueError, match=r"\['pensions'\] have no declared factor"): + validate_money_normalization(declaration) + + +def test_unused_and_unknown_families_refuse(): + declaration = toy_declaration() + declaration["index_family_factors"]["rents"] = { + "identity": True, + "reason": "unused", + } + with pytest.raises(ValueError, match="that no normalized column uses"): + validate_money_normalization(declaration) + declaration = toy_declaration() + declaration["index_family_factors"]["wages"] = {"identity": True, "reason": "x"} + with pytest.raises(ValueError, match="unknown index families"): + validate_money_normalization(declaration) + + +@pytest.mark.parametrize( + "entry", + [ + {"identity": True}, + {"identity": True, "reason": ""}, + {"identity": "yes", "reason": "x"}, + {"identity": False, "reason": "x"}, + {"identity": True, "reason": "x", "factor_id": "f"}, + ], +) +def test_an_identity_family_is_explicit_with_a_reason(entry): + declaration = toy_declaration() + declaration["index_family_factors"]["pensions"] = entry + with pytest.raises(ValueError, match="identity"): + validate_money_normalization(declaration) + + +def test_explicit_identity_is_admitted_and_selected_as_none(): + declaration = validate_money_normalization( + toy_declaration(identity_families=("pensions",)) + ) + _, factors = selected_money_factors(declaration, {}) + assert factors["pensions"] is None + assert factors["capital_income"] == factor_id("central") + + +def test_profiled_families_name_one_factor_per_profile_and_a_disclosure(): + declaration = toy_declaration() + declaration["index_family_factors"]["pensions"]["factors"].pop("high") + with pytest.raises(ValueError, match="every declared profile"): + validate_money_normalization(declaration) + declaration = toy_declaration() + declaration["index_family_factors"]["pensions"].pop("disclosure") + with pytest.raises(ValueError, match="missing fields \\['disclosure'\\]"): + validate_money_normalization(declaration) + + +def test_unprofiled_declarations_use_one_factor_id_per_family(): + declaration = toy_declaration() + declaration["factor_profile"] = None + for entry in declaration["index_family_factors"].values(): + entry.pop("factors") + entry["factor_id"] = factor_id("central") + validated = validate_money_normalization(declaration) + assert selected_money_factors(validated, {DIMENSION: "low"}) == ( + None, + dict.fromkeys(validated["index_family_factors"], factor_id("central")), + ) + declaration["factor_profile"] = { + "dimension": DIMENSION, + "profiles": ["central"], + "central_profile": "central", + } + with pytest.raises(ValueError, match="unknown fields \\['factor_id'\\]"): + validate_money_normalization(declaration) + + +def test_profile_selection_follows_the_knob_and_refuses_unknown_profiles(): + declaration = validate_money_normalization(toy_declaration()) + for selection, expected in ( + ({}, "central"), + ({DIMENSION: "low"}, "low"), + ({DIMENSION: "high", "other_knob": "x"}, "high"), + ): + profile, factors = selected_money_factors(declaration, selection) + assert profile == expected + assert set(factors.values()) == {factor_id(expected)} + with pytest.raises(ValueError, match="Unknown money normalization profile"): + selected_money_factors(declaration, {DIMENSION: "bogus"}) + + +def test_feed_rate_form_is_closed_and_needs_its_expected_decimal(): + identity = { + "source_commit": "c" * 40, + "facts_sha256": "a" * 64, + "manifest_sha256": "b" * 64, + "artifact_schema_version": "policyengine_ledger.consumer_artifact.v2", + "consumer_fact_schema_versions": ["chronicle.consumer_fact.v4"], + "consumer_fact_schema_sha256": "d" * 64, + "fact_row_count": 5, + } + declaration = toy_declaration() + declaration["rate"] = rate_feed_form(identity, "1.0824") + validated = validate_money_normalization(declaration) + assert validated["rate"]["expected_decimal"] == "1.0824" + for edit in ( + lambda rate: rate.pop("expected_decimal"), + lambda rate: rate.update(expected_decimal=1.0824), + lambda rate: rate.update(assertion_policy="anything"), + lambda rate: rate["selector"].update(not_a_selector_field=1), + lambda rate: rate.update(citation={"publisher": "x"}), + ): + broken = copy.deepcopy(declaration) + edit(broken["rate"]) + with pytest.raises(ValueError): + validate_money_normalization(broken) + + +# --------------------------------------------------------------------------- +# Exact arithmetic +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + ("value", "rate", "quote", "factor", "expected"), + [ + # 3 * 1.25 = 3.75 -> 375 cents; 375 * 1.1 = 412.5 ties to even 412. + (3.0, "1.25", "to_per_from", 1100000, 412), + # 5 * 1.25 * 1.1 = 6.875 -> 687.5 ties to even 688. + (5.0, "1.25", "to_per_from", 1100000, 688), + # Negative signed amount: -2.675 * 125 = -334.375 -> -334; * 1.1 = -367.4. + (-2.675, "1.25", "to_per_from", 1100000, -367), + # Binary 2.675 is below 2.675; the decimal tie still rounds to even 268. + (2.675, "1", "to_per_from", 1000000, 268), + # A negative half-cent tie rounds to even: -0.125 -> -12.5 -> -12. + (-0.125, "1", "to_per_from", 1000000, -12), + (-0.135, "1", "to_per_from", 1000000, -14), + # Float multiplication gives 57.49999999999999 rather than the exact tie. + (0.575, "1", "to_per_from", 1000000, 58), + # from_per_to divides by the exact rate: 3 / 0.6 = 5.00 -> 500 cents. + (3.0, "0.6", "from_per_to", 1000000, 500), + # A large amount stays exact: 1e15 * 1.654 = 1.654e15 exactly. + (1e15, "1.654", "to_per_from", 1000000, 165400000000000000), + # And 1e9 / 0.6 = 1666666666.666... -> 166666666667 cents. + (1e9, "0.6", "from_per_to", 1000000, 166666666667), + ], +) +def test_hand_cases_round_half_even_at_each_step(value, rate, quote, factor, expected): + arrays, _, declaration, _ = _normalize( + [value], + rate=rate, + quote=quote, + factor=None if factor == 1000000 else _factor(factor), + ) + cents = arrays["person", "rental_income_cents"] + assert cents.dtype == np.int64 and int(cents[0]) == expected + assert arrays["person", "rental_income"][0] == expected / 100 + + +def test_two_roundings_are_recorded_separately_from_one_rounding(): + # 0.004 * 100 = 0.4 -> 0 cents before ageing; one rounding of 0.4 * 1.5 = 0.6 -> 1. + first, two, one = _oracle(0.004, Fraction(1), "to_per_from", Fraction(3, 2)) + assert (first, two, one) == (0, 0, 1) + arrays, *_ = _normalize([0.004], rate="1", factor=_factor(1500000)) + assert int(arrays["person", "rental_income_cents"][0]) == 0 + + +def test_four_cents_rounds_to_seven_fx_then_eight_indexed_cents(): + rate, factor = Fraction("1.654"), Fraction("1.079073") + assert _oracle(0.04, rate, "to_per_from", factor) == (7, 8, 7) + arrays, receipts, *_ = _normalize([0.04], rate="1.654", factor=_factor(1079073)) + assert int(arrays["person", "rental_income_cents"][0]) == 8 + assert receipts[0]["sum_after_fx"] == "0.07" + assert receipts[0]["sum_after"] == "0.08" + + +def test_decimal_context_does_not_round_products_quotients_or_receipt_sums(): + values = [1e15, 0.04, -1e15, 0.125, -0.135] + expected, expected_receipts, *_ = _normalize( + values, rate="1.654", factor=_factor(1079073) + ) + with localcontext() as context: + context.prec = 2 + actual, receipts, *_ = _normalize(values, rate="1.654", factor=_factor(1079073)) + assert {key: values.tolist() for key, values in actual.items()} == { + key: values.tolist() for key, values in expected.items() + } + assert receipts == expected_receipts + assert receipts[0]["sum_before"] == "0.03" + + +@pytest.mark.parametrize("bad", [math.nan, math.inf, -math.inf]) +def test_nan_and_infinite_amounts_refuse(bad): + with pytest.raises(ValueError, match="NaN and infinite"): + _normalize([1.0, bad]) + + +def test_an_unsigned_column_refuses_a_negative_amount_and_signed_keeps_sign(): + with pytest.raises( + ValueError, match="Unsigned money column person.interest_income" + ): + _normalize([-1.0], column="interest_income") + arrays, *_ = _normalize([-1.0, 1.0]) + assert arrays["person", "rental_income"].tolist() == [-1.25, 1.25] + + +@pytest.mark.parametrize("value", [1e17, -1e17]) +def test_int64_minor_units_bound_refuses_overflow(value): + with pytest.raises(ValueError, match="exceeds int64"): + _normalize([value], rate="1") + + +def test_int64_bound_is_checked_at_both_rounding_steps(): + # The FX cents overflow even though the factor would shrink them into range. + with pytest.raises(ValueError, match="exceeds int64"): + _normalize([1e17], rate="1", factor=_factor(1, digits=1)) + # The FX cents fit, but the factor would overflow the companion column. + with pytest.raises(ValueError, match="exceeds int64"): + _normalize([1e16], rate="1", factor=_factor(10, digits=0)) + + +def test_int64_lower_endpoint_is_a_valid_signed_cent_amount(): + arrays, *_ = _normalize([-1.0], rate="1", factor=_factor(2**63, digits=2)) + assert int(arrays["person", "rental_income_cents"][0]) == -(2**63) + + +@pytest.mark.parametrize( + ("entity", "column"), + [(entity, column) for entity, names in NORMALIZED.items() for column in names], +) +def test_every_declared_cents_column_checks_int64_overflow(entity, column): + declaration, family = _one_column_declaration(column, entity) + with pytest.raises(ValueError, match=f"{entity}.{column} exceeds int64"): + normalize_money_tables( + {entity: pd.DataFrame({column: [1e17]})}, + declaration, + rate=Fraction(1), + rate_text="1", + factors={family: _identity_factor()}, + ) + + +def test_normalizing_twice_refuses(): + arrays, _, declaration, tables = _normalize([1.0, 2.0]) + table = tables["person"].copy() + for (_, column), values in arrays.items(): + table[column] = values + with pytest.raises(ValueError, match="normalized exactly once"): + normalize_money_tables( + {"person": table}, + declaration, + rate=Fraction(5, 4), + rate_text="1.25", + factors={"capital_income": _identity_factor()}, + ) + + +def test_receipt_records_exact_sums_residuals_and_identities(): + _, receipts, *_ = _normalize( + [3.0, 5.0, -2.675, 0.1], factor=_factor(1100000, name="toy") + ) + (receipt,) = receipts + assert receipt["n"] == 4 + assert receipt["sum_before"] == "5.425" + # Currency cents: 375, 625, -334 (from -334.375), 12 (from 12.5). + assert receipt["sum_after_fx"] == "6.78" + # Aged cents: 412 (412.5), 688 (687.5), -367 (-367.4), 13 (13.2). + assert receipt["sum_after"] == "7.46" + assert ( + Fraction(**receipt["fx_rounding_residual_minor_units"]) + == Fraction(678) - Fraction("5.425") * 125 + ) + assert Fraction(**receipt["factor_rounding_residual_minor_units"]) == Fraction( + 746 + ) - Fraction(678) * Fraction(11, 10) + assert receipt["factor"] == { + "factor_id": "toy", + "declaration_sha256": "d" * 64, + "receipt_sha256": "e" * 64, + "operational_factor": {"numerator": 1100000, "denominator": 1000000}, + } + assert receipt["rate_text"] == "1.25" + assert receipt["quote"] == "to_per_from" + assert receipt["donor_money_period"] == toy_declaration()["donor_money_period"] + assert receipt["cents_column"] == "rental_income_cents" + assert receipt["index_family"] == "capital_income" + + +def test_identity_receipt_names_its_reason_and_has_no_factor_residual(): + _, receipts, *_ = _normalize([0.5]) + assert receipts[0]["factor"] == {"identity": True, "reason": "test"} + assert receipts[0]["factor_rounding_residual_minor_units"] == { + "numerator": 0, + "denominator": 1, + } + + +# --------------------------------------------------------------------------- +# Rates and factors +# --------------------------------------------------------------------------- + + +def test_decimal_rate_is_exact_and_never_passes_through_float(): + rate, evidence = resolve_money_rate( + validate_money_normalization(toy_declaration(rate="1.654")) + ) + assert rate == Fraction(827, 500) != Fraction(float("1.654")) + assert evidence["text"] == "1.654" + + +def test_feed_rate_replays_against_the_authenticated_feed(tmp_path): + _, pin, identity, facts = write_money_feed(tmp_path / "feed", rate="1.0824") + declaration = toy_declaration() + declaration["rate"] = rate_feed_form(identity, "1.0824") + validated = validate_money_normalization(declaration) + rate, evidence = resolve_money_rate(validated, facts, pin) + assert rate == Fraction("1.0824") + assert evidence["operand"]["decimal_operand"] == "1.0824" + with pytest.raises(ValueError, match="authenticated feed facts"): + resolve_money_rate(validated) + declaration["rate"]["expected_decimal"] = "1.0825" + with pytest.raises(ValueError, match="replay differs"): + resolve_money_rate(validate_money_normalization(declaration), facts, pin) + declaration["rate"] = rate_feed_form({**identity, "fact_row_count": 1}, "1.0824") + with pytest.raises(ValueError, match="feed identity mismatch"): + resolve_money_rate(validate_money_normalization(declaration), facts, pin) + + +def test_money_factors_carry_their_purpose_and_cover_the_donor_period(tmp_path): + assert MONEY_FACTOR_PURPOSE in FACTOR_PURPOSES + _, _, identity, facts = write_money_feed(tmp_path / "feed") + declaration = validate_money_normalization(toy_declaration()) + context = money_factor_context(identity, facts) + for entry, profile in zip( + context["factors"], ("low", "central", "high"), strict=True + ): + numerator, digits = check_money_factor(entry, declaration) + assert Fraction(numerator, 10**digits) == oracle_factor(profile) + assert money_factor_inputs(entry, declaration)["factor_id"] == factor_id( + profile + ) + profile_context = money_factor_context(identity, facts, purpose="profile_input") + with pytest.raises(ValueError, match="applies only money_normalization_input"): + check_money_factor(profile_context["factors"][1], declaration) + other = copy.deepcopy(declaration) + other["donor_money_period"]["value"] = 2023 + with pytest.raises(ValueError, match="not the declared donor money period"): + check_money_factor(context["factors"][1], other) + + +# --------------------------------------------------------------------------- +# Properties against the independent oracle +# --------------------------------------------------------------------------- + +_AMOUNTS = st.one_of( + st.floats(min_value=-1e9, max_value=1e9, allow_nan=False, allow_infinity=False), + st.integers(min_value=-(10**9), max_value=10**9).map(lambda value: value / 1000), + st.integers(min_value=-(10**6), max_value=10**6).map(lambda value: value / 8), +) +_RATES = st.decimals(min_value="0.001", max_value="99.999", places=3).map(str) +_FACTORS = st.integers(min_value=500000, max_value=2500000) + + +@PROPERTY +@example(values=[2.675, -0.125, 3.0], rate="1.000", factor=1100000, quote="to_per_from") +@example(values=[1e9], rate="0.600", factor=1000000, quote="from_per_to") +@given( + values=st.lists(_AMOUNTS, min_size=1, max_size=12), + rate=_RATES, + factor=_FACTORS, + quote=st.sampled_from(["to_per_from", "from_per_to"]), +) +def test_normalization_equals_the_fraction_oracle(values, rate, factor, quote): + arrays, receipts, *_ = _normalize( + values, rate=rate, quote=quote, factor=_factor(factor) + ) + exact_rate = Fraction(rate) + oracle = [ + _oracle(value, exact_rate, quote, Fraction(factor, 10**6))[1] + for value in values + ] + cents = arrays["person", "rental_income_cents"].tolist() + assert cents == oracle + # The float column is exactly the cents over 100, and cents times one + # hundredth reproduces the oracle's decimal amount. + assert arrays["person", "rental_income"].tolist() == [ + float(Fraction(units, 100)) for units in oracle + ] + assert Fraction(receipts[0]["sum_after"]) == Fraction(sum(oracle), 100) + assert Fraction(receipts[0]["sum_before"]) == sum( + Fraction(repr(float(value))) for value in values + ) + + +@PROPERTY +@given(values=st.lists(_AMOUNTS, min_size=1, max_size=12), rate=_RATES) +def test_normalizing_twice_is_refused(values, rate): + arrays, _, declaration, tables = _normalize(values, rate=rate) + table = tables["person"].copy() + for (_, column), result in arrays.items(): + table[column] = result + with pytest.raises(ValueError, match="normalized exactly once"): + normalize_money_tables( + {"person": table}, + declaration, + rate=Fraction(rate), + rate_text=rate, + factors={"capital_income": _identity_factor()}, + ) + + +@PROPERTY +@given( + values=st.lists(_AMOUNTS, min_size=2, max_size=12), + seed=st.integers(min_value=0, max_value=2**32 - 1), + factor=_FACTORS, +) +def test_permuting_rows_commutes_with_normalization(values, seed, factor): + order = np.random.default_rng(seed).permutation(len(values)) + arrays, receipts, *_ = _normalize(values, factor=_factor(factor)) + shuffled, shuffled_receipts, *_ = _normalize( + [values[index] for index in order], factor=_factor(factor) + ) + for key in arrays: + assert shuffled[key].tolist() == arrays[key][order].tolist() + assert shuffled_receipts == receipts + + +@PROPERTY +@example(value=0.004, rate="1.000", factor=1500000, quote="to_per_from") +@given( + value=_AMOUNTS, + rate=_RATES, + factor=_FACTORS, + quote=st.sampled_from(["to_per_from", "from_per_to"]), +) +def test_two_step_rounding_differs_from_one_step_by_at_most_one_cent( + value, rate, factor, quote +): + exact_factor = Fraction(factor, 10**6) + _, two, one = _oracle(value, Fraction(rate), quote, exact_factor) + assert abs(two - one) <= math.floor(1 + exact_factor / 2) + if exact_factor < 2: + assert abs(two - one) <= 1 + arrays, *_ = _normalize([value], rate=rate, quote=quote, factor=_factor(factor)) + assert int(arrays["person", "rental_income_cents"][0]) == two + + +@PROPERTY +@example(value=0.04, factor=1079073) +@given( + value=_AMOUNTS, + factor=st.sampled_from([1077090, 1079073, 1081052]), +) +def test_the_three_declared_factors_bound_two_step_difference_to_one_cent( + value, factor +): + # For y = x * rate * 100, half-even r satisfies |r(y) - y| <= 1/2. + # Thus |r(r(y)*f) - r(y*f)| <= f/2 + 1 < 2 for each declared f < 2. + # The difference is an integer, so its magnitude is at most one cent. + exact_factor = Fraction(factor, 10**6) + assert exact_factor / 2 + 1 < 2 + _, two, one = _oracle(value, Fraction("1.654"), "to_per_from", exact_factor) + assert abs(two - one) <= 1 + arrays, *_ = _normalize([value], rate="1.654", factor=_factor(factor)) + assert int(arrays["person", "rental_income_cents"][0]) == two diff --git a/packages/microcosm-build/tests/engine_free/shared/test_nz_money_normalization.py b/packages/microcosm-build/tests/engine_free/shared/test_nz_money_normalization.py new file mode 100644 index 0000000000..81a098e79d --- /dev/null +++ b/packages/microcosm-build/tests/engine_free/shared/test_nz_money_normalization.py @@ -0,0 +1,240 @@ +"""New Zealand's declared donor money normalization (G8-w w2a). + +The declaration is spec data; nothing activates it until a New Zealand +transport graph reads it (w3). These checks pin its routes against the concept +schema, its single-source exchange rate, and the generated donor factors. +""" + +from __future__ import annotations + +import json +from fractions import Fraction + +import pytest + +from microcosm.build.money_normalization import ( + check_money_factor, + money_column_rosters, + money_concept_columns, + normalized_column_families, + resolve_money_rate, + selected_money_factors, + validate_money_normalization, +) +from microcosm.build.reference_factors import validate_factor_context +from microcosm.build.transport import compose as compose_module +from microcosm.frame.concepts import TransportRule +from test_support.paths import paths_for + +_SPEC = paths_for("microcosm-build").package / "src/microcosm/build/nz" +_DONOR = { + "donor_cy2024_to_ty2027_low": 1077090, + "donor_cy2024_to_ty2027_central": 1079073, + "donor_cy2024_to_ty2027_high": 1081052, +} + + +def _load(name): + return json.loads((_SPEC / name).read_text()) + + +def _declaration(): + return validate_money_normalization(_load("money_normalization.json")) + + +def _factor(factor_id): + return next( + entry + for entry in _load("reference_aging.json")["factors"] + if entry["declaration"]["id"] == factor_id + ) + + +def test_normalized_routes_follow_the_design_table_and_the_concept_schema(): + declaration = _declaration() + assert (declaration["from_currency"], declaration["to_currency"]) == ("USD", "NZD") + assert declaration["quote"] == "to_per_from" + assert declaration["rounding"] == {"digits": 2, "mode": "half_even"} + period = declaration["donor_money_period"] + assert (period["type"], period["value"], period["basis"]) == ( + "calendar_year", + 2024, + "declared_convention", + ) + assert declaration["cents_suffix"] == "_cents" + assert normalized_column_families(declaration) == { + ("person", "nonfarm_self_employment_income"): "mixed_income", + ("person", "farm_self_employment_income"): "mixed_income", + ("person", "interest_income"): "capital_income", + ("person", "dividend_income"): "capital_income", + ("person", "rental_income"): "capital_income", + ("person", "realized_capital_gains"): "capital_income", + ("person", "private_pension_income"): "pensions", + ("household", "mortgage_interest"): "owner_housing_costs", + ("household", "mortgage_principal"): "owner_housing_costs", + ("household", "property_tax"): "owner_housing_costs", + } + rosters = money_column_rosters(declaration) + assert {key for key, (route, _) in rosters.items() if route != "normalized"} == { + ("person", "employment_income"), + ("household", "rent"), + ("person", "liquid_financial_assets"), + } + assert {route for route, _ in rosters.values()} == {"normalized", "quantile_mapped"} + assert declaration["pending"] == {} + assert rosters["person", "employment_income"][1][0].startswith("P1:") + assert rosters["household", "rent"][1][0].startswith("C1:") + assert [item[:3] for item in rosters["person", "liquid_financial_assets"][1]] == [ + "P3:", + "C2:", + ] + + +def test_every_money_concept_surviving_create_is_routed_and_drop_is_not(): + rosters = money_column_rosters(_declaration()) + surviving = { + key + for key, item in money_concept_columns().items() + if item.transport is not TransportRule.DROP + } + assert set(rosters) == surviving + assert len(surviving) == 13 + assert ("person", "public_pension_income") not in rosters + signed = { + key for key, item in money_concept_columns().items() if item.monetary.signed + } + assert signed == { + ("person", "nonfarm_self_employment_income"), + ("person", "farm_self_employment_income"), + ("person", "rental_income"), + ("person", "realized_capital_gains"), + } + + +def test_every_family_uses_the_donor_factor_for_its_profile_with_a_disclosure(): + declaration = _declaration() + assert declaration["factor_profile"] == { + "dimension": "aging_index", + "profiles": ["low", "central", "high"], + "central_profile": "central", + } + for family, entry in declaration["index_family_factors"].items(): + assert entry["factors"] == { + profile: f"donor_cy2024_to_ty2027_{profile}" + for profile in ("low", "central", "high") + }, family + assert "AWE" in entry["disclosure"], family + assert ( + "provisional pending Max's ruling on non-wage ageing (H3)" + in entry["disclosure"] + ), family + + +def test_donor_money_basis_cites_model_lineage_as_a_declared_convention(): + declaration = _declaration() + evidence = " ".join(declaration["donor_money_period"]["evidence"]) + assert "tools/build_us_fiscal_refresh_release.py:410" in evidence + assert "4320-4328" in evidence and "time_period" in evidence + assert "frame/adapters/policyengine_us.py:1230-1234" in evidence + assert "_time_period" in evidence + assert "not authentication" in evidence + disclosure = declaration["notes"] + assert "declared convention covering flows and asset stocks" in disclosure + assert "consistency check, not authentication" in disclosure + assert "No donor file is opened" in disclosure + + +def test_scenario_knobs_select_their_own_donor_factor(): + declaration = _declaration() + scenarios = _load("scenarios.json") + assert scenarios["central_knobs"]["aging_index"] == "central" + for scenario in scenarios["scenarios"]: + profile, factors = selected_money_factors(declaration, scenario["knobs"]) + expected = {"V1": "low", "V2": "high"}.get(scenario["id"], "central") + assert profile == expected, scenario["id"] + assert set(factors.values()) == {f"donor_cy2024_to_ty2027_{expected}"} + + +@pytest.mark.parametrize("profile", ["low", "central", "high"]) +def test_a_missing_declared_profile_factor_refuses(profile): + declaration = _load("money_normalization.json") + del declaration["index_family_factors"]["capital_income"]["factors"][profile] + with pytest.raises(ValueError, match="one factor for every declared profile"): + validate_money_normalization(declaration) + + +def test_the_rate_has_one_source_and_the_bridge_points_to_it(): + declaration = _declaration() + rate, evidence = resolve_money_rate(declaration) + assert rate == Fraction(827, 500) + assert evidence["citation"]["url"].startswith("https://www.irs.gov/") + assert evidence["citation"]["column"] == "2024" + occurrences = [ + path.name + for path in sorted(_SPEC.rglob("*.json")) + if "1.654" in path.read_text() + ] + assert occurrences == ["money_normalization.json"] + assert _load("money_normalization.json")["rate"]["text"] == "1.654" + bridge = _load("currency_bridge.json") + assert bridge["rate"] == {"resource": "money_normalization.json", "path": ["rate"]} + assert "awaiting D1" not in bridge["status"] + assert "d1043" in bridge["status"] + assert "method-card-D1-adopted.md" in bridge["status"] + + +def test_the_three_donor_factors_are_exact_and_only_money_may_apply_them(): + declaration = _declaration() + context = _load("reference_aging.json") + assert set(validate_factor_context(context)) >= set(_DONOR) + for factor_id, numerator in _DONOR.items(): + entry = _factor(factor_id) + assert entry["operational_factor"] == { + "numerator": numerator, + "denominator": 1000000, + } + assert check_money_factor(entry, declaration) == (numerator, 6) + receipt = entry["receipt"] + assert Fraction(**receipt["denominator_mean"]) == Fraction("1591.925") + assert [slot["period"]["value"] for slot in receipt["source_schedule"]] == [ + "2024-Q1", + "2024-Q2", + "2024-Q3", + "2024-Q4", + ] + assert [slot["decimal_operand"] for slot in receipt["source_schedule"]] == [ + "1563.54", + "1586.32", + "1595.11", + "1622.73", + ] + # The numerator is the TY2024 factors' N_s for the same profile: + # N_s = 1700.68 * (1 + q + q**2 + q**3) / 4 with q = (1 + g)**(1/4). + profile = factor_id.rsplit("_", 1)[1] + ird = _factor(f"ird_ty2024_to_ty2027_{profile}")["receipt"] + assert receipt["growth"] == ird["growth"] + assert receipt["root_bounds"]["radicand"] == ird["root_bounds"]["radicand"] + assert receipt["target_schedule"] == ird["target_schedule"] + for bound in ("lower", "upper"): + root = Fraction(**receipt["root_bounds"][bound]) + assert Fraction(**receipt["numerator_mean_bounds"][bound]) == ( + Fraction("1700.68") * sum(root**index for index in range(4)) / 4 + ) + assert entry["declaration"]["purpose"] == "money_normalization_input" + + +@pytest.mark.parametrize("profile", ["low", "central", "high"]) +def test_a_tax_year_factor_is_refused_on_donor_money(profile): + entry = _factor(f"ird_ty2024_to_ty2027_{profile}") + with pytest.raises(ValueError, match="applies only money_normalization_input"): + check_money_factor(entry, _declaration()) + # Even relabelled, its April-to-March schedule is not calendar 2024. + relabelled = json.loads(json.dumps(entry)) + relabelled["declaration"]["purpose"] = "money_normalization_input" + with pytest.raises(ValueError, match="not the declared donor money period"): + check_money_factor(relabelled, _declaration()) + + +def test_no_graph_reads_it_yet_so_preflight_names_no_money_gap(): + spec = compose_module.load_transport_spec("nz") + assert compose_module._money_producer_gaps(spec) == [] diff --git a/packages/microcosm-build/tests/engine_free/shared/test_nz_reference_aging.py b/packages/microcosm-build/tests/engine_free/shared/test_nz_reference_aging.py index fa9ecb3aca..0bf245c6bf 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_nz_reference_aging.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_nz_reference_aging.py @@ -41,7 +41,15 @@ "p1_ty2026_to_ty2027_central": 1028580, "p1_ty2026_to_ty2027_high": 1030467, "observed_ty2026_over_ty2024": 1086069, + "donor_cy2024_to_ty2027_low": 1077090, + "donor_cy2024_to_ty2027_central": 1079073, + "donor_cy2024_to_ty2027_high": 1081052, } +_DENOMINATORS = { + "p1_": Fraction("1670.0725"), + "donor_": Fraction("1591.925"), +} +_PURPOSES = {"observed_": "diagnostic", "donor_": "money_normalization_input"} def _document(): @@ -127,7 +135,7 @@ def test_factor_currency_uses_inclusive_declared_boundaries(today, current): @pytest.mark.parametrize("factor_id", list(_EXPECTED)) -def test_all_six_factors_and_observed_diagnostic_are_exact(factor_id, operand_facts): +def test_all_nine_factors_and_observed_diagnostic_are_exact(factor_id, operand_facts): entry = _entry(factor_id) declaration = entry["declaration"] expected = {"numerator": _EXPECTED[factor_id], "denominator": 1000000} @@ -139,11 +147,17 @@ def test_all_six_factors_and_observed_diagnostic_are_exact(factor_id, operand_fa assert receipt["operation"] == "projected_period_average_ratio" assert receipt["operation_version"] == 1 assert receipt["subperiods_per_year"] == 4 - assert _fraction(receipt["denominator_mean"]) == ( - Fraction("1670.0725") if factor_id.startswith("p1_") else Fraction("1537.7225") + assert _fraction(receipt["denominator_mean"]) == next( + ( + value + for prefix, value in _DENOMINATORS.items() + if factor_id.startswith(prefix) + ), + Fraction("1537.7225"), ) - assert declaration["purpose"] == ( - "diagnostic" if factor_id.startswith("observed_") else "profile_input" + assert declaration["purpose"] == next( + (value for prefix, value in _PURPOSES.items() if factor_id.startswith(prefix)), + "profile_input", ) replay_reference_factor( declaration, operand_facts, _document()["feed_identity"], factor, receipt @@ -288,3 +302,50 @@ def test_september_observation_reanchors_and_requires_reviewed_redeclaration( assert slot["projection_anchor"] == { "aggregate_fact_key": september["aggregate_fact_key"] } + + +def test_donor_factors_average_calendar_2024_and_share_the_tax_year_numerator( + operand_facts, +): + """Hub-recomputed values (G8-w rulings 4, H1); the feed calculation governs.""" + for profile, numerator in ( + ("low", 1077090), + ("central", 1079073), + ("high", 1081052), + ): + entry = _entry(f"donor_cy2024_to_ty2027_{profile}") + ird = _entry(f"ird_ty2024_to_ty2027_{profile}") + declaration = entry["declaration"] + assert [ + slot["validation"]["period_coverage"] + for slot in declaration["source_schedule"] + ][0]["start_date"] == "2024-01-01" + assert ( + declaration["source_schedule"][-1]["validation"]["period_coverage"][ + "end_date" + ] + == "2024-12-31" + ) + assert declaration["target_schedule"] == ird["declaration"]["target_schedule"] + assert declaration["growth"] == ird["declaration"]["growth"] + assert declaration["validity"] == ird["declaration"]["validity"] + factor, receipt = resolve_reference_factor( + declaration, operand_facts, _document()["feed_identity"] + ) + assert factor == {"numerator": numerator, "denominator": 1000000} + mean = ( + Fraction("1563.54") + + Fraction("1586.32") + + Fraction("1595.11") + + Fraction("1622.73") + ) / 4 + assert mean == Fraction("1591.925") == _fraction(receipt["denominator_mean"]) + for bound in ("lower", "upper"): + assert ( + round( + _fraction(ird["receipt"]["numerator_mean_bounds"][bound]) + / mean + * 1000000 + ) + == numerator + ) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py b/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py index 6a30848d75..7cd78076ce 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py @@ -3912,13 +3912,24 @@ def test_mc15_ruled_thresholds_and_tracked_targets_are_declared(self) -> None: } def test_currency_bridge_is_one_declared_multiplication(self) -> None: + # The executable rate lives once, as cited decimal text, in + # money_normalization.json; the MC4 record points to it. bridge = _load("currency_bridge.json") assert (bridge["from_currency"], bridge["to_currency"]) == ("USD", "NZD") assert bridge["operation"] == "multiply" - assert bridge["rounding"] == {"decimal_places": 2} - assert isinstance(bridge["rate"], float) and bridge["rate"] > 0 - assert bridge["source"]["url"].startswith("https://www.irs.gov/") - assert bridge["source"]["column"] == str(bridge["rate_year"]) + assert bridge["rate"] == { + "resource": "money_normalization.json", + "path": ["rate"], + } + money = _load("money_normalization.json") + rate = money[bridge["rate"]["path"][0]] + assert money["quote"] == "to_per_from" + assert money["rounding"] == {"digits": 2, "mode": "half_even"} + assert rate["form"] == "decimal_text" + assert isinstance(rate["text"], str) and float(rate["text"]) > 0 + assert rate["citation"]["url"].startswith("https://www.irs.gov/") + assert rate["citation"]["column"] == str(bridge["rate_year"]) + assert "awaiting D1" not in bridge["status"] def test_benefit_unit_rule_declares_the_settled_dependent_child_age(self) -> None: rule = _load("benefit_unit_rule.json") diff --git a/packages/microcosm-build/tests/engine_free/shared/test_reference_factors.py b/packages/microcosm-build/tests/engine_free/shared/test_reference_factors.py index 6e2045bbd8..ce3ffe35c5 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_reference_factors.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_reference_factors.py @@ -12,6 +12,7 @@ from microcosm.build.ledger_artifact import LexicalFloat from microcosm.build.reference_factors import ( + FACTOR_PURPOSES, certified_nth_root_bounds, factor_document_sha256, replay_reference_factor, @@ -605,3 +606,19 @@ def ambiguous_bounds(value, degree, decimal_places): with pytest.raises(ValueError, match="supported exact receipt precision"): resolve_reference_factor(declaration, facts, identity) assert precisions == [26, 52, 104, 208] + + +@pytest.mark.parametrize("purpose", sorted(FACTOR_PURPOSES)) +def test_factor_purposes_are_closed_and_additive(purpose): + assert FACTOR_PURPOSES == { + "profile_input", + "diagnostic", + "money_normalization_input", + } + declaration, facts, identity = factor_fixture() + declaration["purpose"] = purpose + factor, receipt = resolve_reference_factor(declaration, facts, identity) + assert factor == declaration["expected_operational_factor"] + declaration["purpose"] = "money" + with pytest.raises(ValueError, match="Unknown factor purpose"): + resolve_reference_factor(declaration, facts, identity) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py b/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py index 0f05ff89c3..f80863809e 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py @@ -915,6 +915,36 @@ def test_legacy_receipt_remains_version_one_without_factor_fields(): ) +@pytest.mark.parametrize( + ("selection", "digest"), + [ + ({}, "e0e36d480868e0d45cb1862e21b00753ba55f5d8500d8fa4c1b1fdcf442395e0"), + ( + {"aging_index": "low"}, + "406d07423c76eddbbc08601b5bd43e9f84b6b60831d5b18fb22edf3121de03c6", + ), + ( + {"aging_index": "high"}, + "bcf613d7d3ea17c063cfd944bf6df132c9526700d3da2679d5e943113a3e83c8", + ), + ], +) +def test_nz_factor_receipts_are_byte_identical_after_additive_factors( + selection, digest +): + # Canonical v2 receipts produced at d28817ff, before the donor factors and + # the money_normalization_input purpose were added to the factor document. + spec = paths_for("microcosm-build").package / "src/microcosm/build/nz" + _, _, receipt = resolve_reference_profiles( + json.loads((spec / "target_references.json").read_text()), + json.loads((spec / "reference_profiles.json").read_text()), + selection, + factor_context=json.loads((spec / "reference_aging.json").read_text()), + ) + assert receipt["resolver_version"] == "reference-profiles-v2" + assert receipt["receipt_sha256"] == digest + + def test_legacy_receipt_without_transformed_names_refuses_as_a_value_error(): references, declarations = _inputs() effective, _, receipt = resolve_reference_profiles(references, declarations, {}) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_country_bundles.py b/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_country_bundles.py index e7a2b0e2a6..2b41d3c216 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_country_bundles.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_country_bundles.py @@ -232,6 +232,7 @@ def test_nz_generation_zero_views_come_from_the_country_spec_seam() -> None: "gates.json", "geography_spine.json", "holdout_references.json", + "money_normalization.json", "precal_references.json", "reference_aging.json", "reference_intervals.json", diff --git a/packages/microcosm-build/tests/engine_free/shared/test_transport_money_kernels.py b/packages/microcosm-build/tests/engine_free/shared/test_transport_money_kernels.py new file mode 100644 index 0000000000..1149b9d419 --- /dev/null +++ b/packages/microcosm-build/tests/engine_free/shared/test_transport_money_kernels.py @@ -0,0 +1,804 @@ +"""``money.normalize@1``, its opt-in producer preflight and the toy lineage run. + +Invariants executed here: + +- Each declared money column is normalized exactly once per lineage: the + kernel rewrites it once with its integer companion, composition refuses a + second normalization in any version's lineage (a FILTER carries every + column of its base version), and a consumed column needs exactly one + declared producer. +- Factor authenticity: a factor whose receipt is self-consistent but whose + operands differ from the authenticated feed refuses on replay. +- The donor money period recorded by CREATE must equal the declaration's. +- A scenario's profile knob selects its own factor (V1 low, V2 high). +- Opt-in: without a money-normalization resource, preflight and composition + are byte-identical and the toy keeps ``transport.currency@1``. +""" + +from __future__ import annotations + +import copy +import json +from dataclasses import replace +from fractions import Fraction + +import numpy as np +import pandas as pd +import pytest + +from microcosm.build.reference_factors import ( + resolve_reference_factor, + validate_factor_context, +) +from microcosm.build.transport import compose as compose_module +from microcosm.build.transport import money_kernels as money_kernels_module +from microcosm.build.transport.artifact_types import ( + INCOME_CELLS_TYPE, + TARGET_SURFACE_TYPE, +) +from microcosm.build.transport.codecs import register_transport_codecs +from microcosm.build.transport.compose import ( + SKELETON_KERNELS, + validate_transport_activation, +) +from microcosm.build.transport.money_kernels import MONEY_NORMALIZE +from microcosm.build.transport.registry import register_transport_population_kernels +from microcosm.graph import ( + SOURCE_CODECS, + ArtifactInput, + ArtifactOutput, + ContentStore, + KernelContext, + KernelRegistry, + KernelResult, + Node, + Owned, + Slice, + compile_graph, + graph_to_json, + run_graph, +) +from microcosm.graph.canonical import canonical_json +from test_support.microcosm_build.money_normalization import ( + DIMENSION, + GRAPH_NORMALIZED, + RAW_FARM, + RAW_RENTAL, + ToyMoneyConsumer, + add_money_columns, + factor_id, + graph_declaration, + money_factor_context, + money_graph, + money_registry, + money_sources, + normalize_node, + oracle_factor, + rate_feed_form, + write_money_feed, +) +from test_support.microcosm_build.transport_composed import make_composed_fixture +from test_support.microcosm_build.transport_population import ( + write_population_sources, +) + +_VALUES = { + "interest_income": [3.0, 5.0, 0.1], + "rental_income": [-120.5, 0.0, 35.25], + "farm_self_employment_income": [0.0, -2.675, 1000000.0], +} + + +@pytest.fixture(scope="module") +def feed(tmp_path_factory): + path, pin, identity, facts = write_money_feed( + tmp_path_factory.mktemp("money-feed"), rate="1.25" + ) + return path, pin, identity, facts, money_factor_context(identity, facts) + + +def _oracle_cents(value: float, rate: Fraction, factor: Fraction) -> int: + return round(round(Fraction(repr(value)) * rate * 100) * factor) + + +def _context( + feed, + *, + declaration=None, + factor_context=None, + selection=None, + metadata=None, + pin=True, + node=None, +): + path, feed_pin, _, _, context = feed + declaration = graph_declaration() if declaration is None else declaration + factor_context = context if factor_context is None else factor_context + node = node or normalize_node( + "toy.normalize", + "toy.central", + declaration, + factor_context, + feed_pin, + selection=selection, + ) + params = dict(node.params) + if not pin: + params.pop("chronicle_feed_pin") + return KernelContext( + node=node, + tables={ + "person": pd.DataFrame({"person_id": [11, 12, 13], **_VALUES}), + }, + weights={}, + strata=pd.Series([], dtype=object), + params=params, + rng=np.random.default_rng(0), + sources={"money_facts": path}, + frame_metadata=({"donor_time_period": 2024} if metadata is None else metadata), + ) + + +# --------------------------------------------------------------------------- +# The kernel +# --------------------------------------------------------------------------- + + +def test_kernel_rewrites_each_column_once_with_its_companion_and_receipt(feed): + result = MONEY_NORMALIZE.run(_context(feed)) + factor = oracle_factor("central") + for column, values in _VALUES.items(): + cents = [_oracle_cents(value, Fraction(5, 4), factor) for value in values] + assert result.columns["person", f"{column}_cents"].tolist() == cents + assert result.columns["person", column].tolist() == [c / 100 for c in cents] + assert result.columns["person", column].index.tolist() == [11, 12, 13] + # 3 * 1.25 * 1.1 = 4.125 ties to even; -2.675 * 1.25 = -3.34375. + assert result.columns["person", "interest_income_cents"].tolist()[0] == 412 + assert result.columns["person", "farm_self_employment_income_cents"].tolist()[ + 1 + ] == round(-334 * Fraction(11, 10)) + receipt = result.receipt + assert receipt["profile"] == "central" + assert receipt["recorded_donor_time_period"] == 2024 + assert receipt["rate"]["text"] == "1.25" + assert {row["factor_id"] for row in receipt["factors"].values()} == { + factor_id("central") + } + assert [row["column"] for row in receipt["columns"]] == GRAPH_NORMALIZED["person"] + assert all(row["n"] == 3 for row in receipt["columns"]) + + +@pytest.mark.parametrize( + ("selection", "profile"), + [ + ({}, "central"), + ({DIMENSION: "low"}, "low"), + ({DIMENSION: "high"}, "high"), + ({DIMENSION: "central", "unrelated": "x"}, "central"), + ], +) +def test_the_profile_knob_selects_each_variants_own_factor(feed, selection, profile): + result = MONEY_NORMALIZE.run(_context(feed, selection=selection)) + assert result.receipt["profile"] == profile + expected = [ + _oracle_cents(value, Fraction(5, 4), oracle_factor(profile)) + for value in _VALUES["rental_income"] + ] + assert result.columns["person", "rental_income_cents"].tolist() == expected + + +def test_an_unknown_profile_refuses(feed): + with pytest.raises(ValueError, match="Unknown money normalization profile"): + MONEY_NORMALIZE.run(_context(feed, selection={DIMENSION: "bogus"})) + + +def test_a_missing_declared_profile_factor_refuses(feed): + declaration = graph_declaration() + del declaration["index_family_factors"]["capital_income"]["factors"]["low"] + with pytest.raises(ValueError, match="one factor for every declared profile"): + MONEY_NORMALIZE.run( + _context(feed, declaration=declaration, selection={DIMENSION: "low"}) + ) + + +@pytest.mark.parametrize( + ("metadata", "message"), + [ + ({"donor_time_period": 2023}, "differs from the declared calendar_year 2024"), + ({"donor_time_period": "2024"}, "differs from the declared"), + ({"donor_time_period": None}, "records no money period"), + ({}, "needs the frame's recorded donor_time_period"), + ], +) +def test_a_donor_period_mismatch_or_null_refuses(feed, metadata, message): + with pytest.raises(ValueError, match=message): + MONEY_NORMALIZE.run(_context(feed, metadata=metadata)) + + +def test_a_self_consistent_tampered_factor_refuses_on_feed_replay(feed): + _, _, identity, facts, context = feed + forged_facts = copy.deepcopy(list(facts)) + anchor = next( + row for row in forged_facts if row["aggregate_fact_key"] == "anchor-0" + ) + anchor["value"] = 120 + forged = copy.deepcopy(context) + entry = forged["factors"][1] + entry["declaration"]["expected_operational_factor"]["numerator"] = 1200000 + factor, receipt = resolve_reference_factor( + entry["declaration"], forged_facts, identity + ) + entry["operational_factor"], entry["receipt"] = factor, receipt + # The forged context is internally consistent ... + assert set(validate_factor_context(forged)) == { + factor_id(profile) for profile in ("low", "central", "high") + } + # ... but replay against the authenticated feed refuses it. + with pytest.raises(ValueError, match="expected factor mismatch|replay differs"): + MONEY_NORMALIZE.run(_context(feed, factor_context=forged)) + + +def test_without_a_feed_pin_the_factor_source_commit_must_be_authenticated(feed): + with pytest.raises(ValueError, match="cannot be authenticated without a feed pin"): + MONEY_NORMALIZE.run(_context(feed, pin=False)) + + +def test_a_profile_input_factor_is_never_applied_to_donor_money(feed): + _, _, identity, facts, _ = feed + context = money_factor_context(identity, facts, purpose="profile_input") + with pytest.raises(ValueError, match="applies only money_normalization_input"): + MONEY_NORMALIZE.run(_context(feed, factor_context=context)) + + +def test_a_feed_selected_rate_replays_through_the_kernel(feed): + _, _, identity, _, _ = feed + declaration = graph_declaration() + declaration["rate"] = rate_feed_form(identity, "1.25") + result = MONEY_NORMALIZE.run(_context(feed, declaration=declaration)) + assert result.receipt["rate"]["form"] == "feed_selector" + assert result.receipt["rate"]["operand"]["decimal_operand"] == "1.25" + assert result.columns["person", "interest_income_cents"].tolist()[0] == 412 + declaration["rate"]["expected_decimal"] = "1.26" + with pytest.raises(ValueError, match="replay differs"): + MONEY_NORMALIZE.run(_context(feed, declaration=declaration)) + + +def test_outputs_and_slices_must_equal_the_declared_columns(feed): + _, pin, _, _, context = feed + node = normalize_node( + "toy.normalize", "toy.central", graph_declaration(), context, pin + ) + short = Node( + node.id, + node.kernel, + population=node.population, + sources=node.sources, + inputs=node.inputs, + outputs=node.outputs[:-1], + params=node.params, + ) + with pytest.raises(ValueError, match="rewrite exactly the declared money columns"): + MONEY_NORMALIZE.run(_context(feed, node=short)) + wide = Node( + node.id, + node.kernel, + population=node.population, + sources=node.sources, + inputs=(Slice("person", (*GRAPH_NORMALIZED["person"], "age")),), + outputs=node.outputs, + params=node.params, + ) + with pytest.raises(ValueError, match="slices exactly the declared money columns"): + MONEY_NORMALIZE.run(_context(feed, node=wide)) + typed = Node( + node.id, + node.kernel, + population=node.population, + sources=node.sources, + inputs=node.inputs, + outputs=node.outputs, + params=node.params, + artifact_outputs=(ArtifactOutput("surface", TARGET_SURFACE_TYPE),), + ) + with pytest.raises(ValueError, match="no artifacts"): + MONEY_NORMALIZE.run(_context(feed, node=typed)) + + +def test_implementation_hash_binds_the_pure_module_and_factor_replay(monkeypatch): + seen = [] + + def recording(*objects, dependencies=()): + seen.extend(objects) + return "0" * 64 + + monkeypatch.setattr(money_kernels_module, "source_hash", recording) + MONEY_NORMALIZE.implementation_hash() + names = {getattr(item, "__name__", "") for item in seen} + assert { + "microcosm.build.money_normalization", + "microcosm.build.reference_factors", + "microcosm.build.ledger_artifact", + "microcosm.build.ledger_targets", + "microcosm.frame.concepts", + } <= names + monkeypatch.undo() + assert ( + MONEY_NORMALIZE.implementation_hash() == MONEY_NORMALIZE.implementation_hash() + ) + + +def test_the_kernel_is_registered_and_a_skeleton_kernel(): + registry = KernelRegistry() + register_transport_population_kernels(registry) + assert "money.normalize@1" in registry.refs() + assert "money.normalize@1" in SKELETON_KERNELS + assert "nz" not in MONEY_NORMALIZE.ref + + +def test_composition_binds_the_country_feed_pin_to_the_normalizer(): + spec = { + "country": "xx", + "resources": {"chronicle_feed": {"source_commit": "c" * 40, "x": 1}}, + } + params = compose_module._target_feed_params(spec, "money.normalize@1", {}) + assert json.loads(params["chronicle_feed_pin"])["source_commit"] == "c" * 40 + assert "chronicle_feed_pin" not in compose_module._target_feed_params( + spec, "transport.currency@1", {} + ) + + +# --------------------------------------------------------------------------- +# The opt-in producer preflight +# --------------------------------------------------------------------------- + + +def _row(node_id, kernel, population, inputs=(), outputs=(), **fields): + return { + "id": node_id, + "kernel": kernel, + "population": population, + "inputs": [{"entity": e, "columns": list(c)} for e, c in inputs], + "outputs": [ + {"entity": e, "column": c, "dtype": "float64", "rewrite": True} + for e, c in outputs + ], + **fields, + } + + +def _structural(node_id, base, kind="filter"): + return { + "id": node_id, + "kernel": "transport.boundary@1", + "structural": kind, + "base": base, + } + + +_CREATE = {"id": "c", "kernel": "transport.create@1", "structural": "create"} + + +def _spec(*rows, declaration=None): + return { + "country": "xx", + "resources": { + "money_normalization": graph_declaration() + if declaration is None + else declaration, + "transport_graph": {"nodes": [_CREATE, _structural("open", "c"), *rows]}, + }, + } + + +def _normalizer(node_id, population): + return _row( + node_id, + "money.normalize@1", + population, + inputs=[("person", GRAPH_NORMALIZED["person"])], + outputs=[("person", column) for column in GRAPH_NORMALIZED["person"]], + ) + + +def _reader(node_id, population, *columns, entity="person"): + return _row(node_id, "concepts.encode@1", population, inputs=[(entity, columns)]) + + +def test_a_normalized_column_read_after_its_normalizer_has_no_gap(): + spec = _spec( + _structural("central", "open"), + _normalizer("central.n", "central"), + _reader("central.read", "central", "interest_income", "rental_income"), + ) + assert compose_module._money_producer_gaps(spec) == [] + + +def test_a_consumed_pending_column_is_a_named_gap(): + spec = _spec(_reader("open.read", "open", "dividend_income")) + assert compose_module._money_producer_gaps(spec) == [ + "money column person.dividend_income awaits a donor that carries it " + "(read by ['open.read'])" + ] + + +def test_a_consumed_quantile_mapped_column_without_its_map_names_what_it_awaits(): + spec = _spec( + _reader("open.read", "open", "employment_income"), + _reader("open.rent", "open", "rent", entity="household"), + ) + assert compose_module._money_producer_gaps(spec) == [ + "money column household.rent awaits quantile map <- toy rent bands " + "(read by ['open.rent'])", + "money column person.employment_income awaits quantile map <- toy wage " + "bands (read by ['open.read'])", + ] + + +def test_raw_donor_money_read_without_its_normalizer_is_a_gap(): + gaps = compose_module._money_producer_gaps( + _spec(_reader("open.read", "open", "interest_income")) + ) + assert gaps == [ + "money column person.interest_income has no money.normalize@1 producer " + "(read by ['open.read'])" + ] + + +def test_a_second_normalization_in_one_lineage_is_a_gap(): + spec = _spec( + _normalizer("open.n", "open"), + _structural("central", "open"), + _normalizer("central.n", "central"), + _reader("central.read", "central", "interest_income"), + ) + (gap,) = compose_module._money_producer_gaps(spec) + assert gap.startswith( + "money column person.interest_income has 2 producers in one lineage " + "['central.n', 'open.n']" + ) + + +def test_variant_lineages_are_independent(): + # A V1-style FILTER on the shared version carries its base, never a sibling. + spec = _spec( + _structural("central", "open"), + _normalizer("central.n", "central"), + _reader("central.read", "central", "interest_income"), + _structural("v1", "open"), + _normalizer("v1.n", "v1"), + _reader("v1.read", "v1", "interest_income"), + _structural("v1.after", "v1"), + _reader("v1.after.read", "v1.after", "interest_income"), + ) + assert compose_module._money_producer_gaps(spec) == [] + + +def test_only_the_two_money_producers_may_produce_a_money_column(): + spec = _spec( + _row( + "open.currency", + "transport.currency@1", + "open", + inputs=[("person", ["interest_income"])], + outputs=[("person", "interest_income")], + ), + _reader("open.read", "open", "interest_income"), + ) + (gap,) = compose_module._money_producer_gaps(spec) + assert "produced by transport.currency@1 node 'open.currency'" in gap + + +def test_a_quantile_map_cannot_produce_a_normalized_column(): + spec = _spec( + _row( + "open.qmap", + "transport.quantile_map@1", + "open", + inputs=[("person", ["interest_income"])], + outputs=[("person", "interest_income")], + ), + _reader("open.read", "open", "interest_income"), + ) + (gap,) = compose_module._money_producer_gaps(spec) + assert "declared normalized but produced by transport.quantile_map@1" in gap + + +def test_a_quantile_mapped_drop_default_column_has_its_map_as_sole_producer(): + declaration = graph_declaration() + declaration["quantile_mapped"]["person"]["public_pension_income"] = { + "awaits": ["toy pension bands"], + "reason": "Quantile-mapped although its concept default is drop.", + } + spec = _spec( + _row( + "open.qmap", + "transport.quantile_map@1", + "open", + inputs=[("person", ["public_pension_income"])], + outputs=[("person", "public_pension_income")], + ), + _reader("open.read", "open", "public_pension_income"), + declaration=declaration, + ) + assert compose_module._money_producer_gaps(spec) == [] + + +def test_a_new_destination_money_output_is_derived_instead_of_donor_money(): + derived = _row( + "open.pension", + "simulate.rules_by_ref@1", + "open", + outputs=[("person", "public_pension_income")], + ) + derived["outputs"][0]["rewrite"] = False + spec = _spec( + derived, + _reader("open.read", "open", "public_pension_income"), + ) + assert compose_module._money_producer_gaps(spec) == [] + + +def test_an_invalid_declaration_is_a_named_preflight_gap(): + declaration = graph_declaration() + declaration["rate"]["text"] = 1.25 + (gap,) = compose_module._money_producer_gaps(_spec(declaration=declaration)) + assert gap.startswith("money_normalization.json: ") + + +def test_without_the_resource_preflight_reads_nothing(): + spec = {"country": "xx", "resources": {"transport_graph": {"nodes": "malformed"}}} + assert compose_module._money_producer_gaps(spec) == [] + compose_module._check_single_money_normalization(spec, None, None) + + +def test_activation_names_money_gaps_only_when_the_toy_opts_in(tmp_path, monkeypatch): + fixture = make_composed_fixture(tmp_path / "toy") + validate_transport_activation(fixture.spec) + graph = fixture.graph() + kernels = {node.kernel for node in graph.nodes} + assert "transport.currency@1" in kernels + assert "money.normalize@1" not in kernels + plain = graph_to_json(graph) + extended = graph_to_json(fixture.graph(extended=True)) + # With the hooks removed, composition and activation are byte-identical. + with monkeypatch.context() as patch: + patch.setattr(compose_module, "_money_producer_gaps", lambda spec: []) + patch.setattr( + compose_module, "_check_single_money_normalization", lambda *args: None + ) + assert graph_to_json(fixture.graph()) == plain + assert graph_to_json(fixture.graph(extended=True)) == extended + validate_transport_activation(fixture.spec) + opted = copy.deepcopy(fixture.spec) + opted["resources"]["money_normalization"] = graph_declaration() + with pytest.raises(ValueError) as error: + validate_transport_activation(opted) + assert ( + "money column person.interest_income is produced by transport.currency@1 " + "node 'nz.transport.currency'" + ) in str(error.value) + + +# --------------------------------------------------------------------------- +# The toy lineage run: CREATE -> open -> {central, v1} +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def toy_run(tmp_path_factory, feed): + root = tmp_path_factory.mktemp("money-graph") + population = write_population_sources( + root / "population", time_period=2024, edit_tables=add_money_columns + ) + path, pin, _, _, context = feed + declaration = graph_declaration() + register_transport_codecs(SOURCE_CODECS) + graph = money_graph(population, declaration, context, pin) + manifest = run_graph( + compile_graph(graph), + sources=money_sources(population, path), + store=ContentStore(root / "store"), + kernels=money_registry(), + ) + return population, declaration, context, pin, graph, manifest + + +def test_each_branch_normalizes_once_with_its_own_factor(toy_run): + population, declaration, _, _, graph, manifest = toy_run + spec = {"country": "xx", "resources": {"money_normalization": declaration}} + compose_module._check_single_money_normalization(spec, graph, compile_graph(graph)) + created = manifest.population("nz.create").table("person") + raw = {column: created[column].tolist() for column in GRAPH_NORMALIZED["person"]} + assert raw["rental_income"] == RAW_RENTAL + assert raw["farm_self_employment_income"] == RAW_FARM + for branch, profile in ( + ("toy.central", "central"), + ("toy.v1", "low"), + ("toy.v2", "high"), + ): + receipt = manifest.nodes[f"{branch}.consume"].receipt + factor = oracle_factor(profile) + for column in GRAPH_NORMALIZED["person"]: + cents = [ + _oracle_cents(value, Fraction(5, 4), factor) for value in raw[column] + ] + assert list(receipt[f"{column}_cents"]) == cents, (branch, column) + assert list(receipt[column]) == [c / 100 for c in cents] + normalize = manifest.nodes[f"{branch}.normalize"].receipt + assert normalize["profile"] == profile + assert normalize["recorded_donor_time_period"] == 2024 + # The shared version is untouched: normalization lives on each branch. + assert ( + manifest.population("toy.open").table("person")["rental_income"].tolist() + == RAW_RENTAL + ) + assert "rental_income_cents" not in manifest.population("toy.open").table("person") + + +def test_create_records_the_donor_money_period_for_the_branches(toy_run): + *_, manifest = toy_run + assert manifest.nodes["nz.create"].receipt["donor_time_period"] == 2024 + for version in ("nz.create", "toy.open", "toy.central", "toy.v1", "toy.v2"): + assert manifest.population(version).metadata["donor_time_period"] == 2024 + + +def test_a_second_normalization_in_a_shared_version_refuses_at_composition(toy_run): + population, declaration, context, pin, _, _ = toy_run + doubled = money_graph(population, declaration, context, pin, normalize_open=True) + spec = {"country": "xx", "resources": {"money_normalization": declaration}} + with pytest.raises(ValueError, match="normalized more than once in the lineage"): + compose_module._check_single_money_normalization( + spec, doubled, compile_graph(doubled) + ) + + +class _ToyBaselineCells(ToyMoneyConsumer): + """Read normalized branch money and exchange baseline reporting evidence.""" + + ref = "toy.baseline_cells@1" + + def run(self, context): + result = super().run(context) + if context.artifacts: + baseline = json.loads(context.artifacts["baseline_cells"].payload) + return KernelResult( + receipt={**result.receipt, "baseline_cells": baseline["person_id"]} + ) + return replace( + result, + artifacts={"baseline_cells": canonical_json(dict(result.receipt))}, + ) + + +def test_s0_v1_normalize_once_despite_a_cross_population_baseline_artifact( + tmp_path, toy_run, feed +): + population, declaration, _, _, graph, _ = toy_run + baseline = replace( + graph.node("toy.central.consume"), + kernel=_ToyBaselineCells.ref, + artifact_outputs=(ArtifactOutput("baseline_cells", INCOME_CELLS_TYPE),), + ) + variant = replace( + graph.node("toy.v1.consume"), + kernel=_ToyBaselineCells.ref, + artifact_inputs=( + ArtifactInput( + "baseline_cells", baseline.id, "baseline_cells", INCOME_CELLS_TYPE + ), + ), + ) + graph = replace( + graph, + nodes=tuple( + baseline + if node.id == baseline.id + else variant + if node.id == variant.id + else node + for node in graph.nodes + ), + ) + compiled = compile_graph(graph) + # The artifact makes both normalizers DAG ancestors of V1's consumer. + # It does not make S0 a structural base of V1 or transfer its ownership. + ancestors = compose_module._ancestor_closure(compiled.predecessors, {variant.id}) + assert {"toy.central.normalize", "toy.v1.normalize"} <= ancestors + spec = {"country": "xx", "resources": {"money_normalization": declaration}} + compose_module._check_single_money_normalization(spec, graph, compiled) + registry = money_registry() + registry.register(_ToyBaselineCells()) + manifest = run_graph( + compiled, + sources=money_sources(population, feed[0]), + store=ContentStore(tmp_path / "store"), + kernels=registry, + ) + assert ( + manifest.nodes[variant.id].receipt["baseline_cells"] + == manifest.nodes[baseline.id].receipt["person_id"] + ) + assert manifest.nodes["toy.v1.normalize"].receipt["profile"] == "low" + assert manifest.nodes["toy.central.normalize"].receipt["profile"] == "central" + + +def test_currency_then_normalization_refuses_on_the_same_money_lineage(toy_run): + population, declaration, context, pin, _, _ = toy_run + graph = money_graph(population, declaration, context, pin) + currency = Node( + "toy.open.currency", + "transport.currency@1", + population="toy.open", + inputs=(Slice("person", ("interest_income",)),), + outputs=(Owned("person", "interest_income", "float64", rewrite=True),), + ) + graph = replace(graph, nodes=(*graph.nodes, currency)) + spec = {"country": "xx", "resources": {"money_normalization": declaration}} + with pytest.raises(ValueError, match="normalized more than once.*interest_income"): + compose_module._check_single_money_normalization( + spec, graph, compile_graph(graph) + ) + + +def test_derived_money_does_not_add_a_donor_conversion_to_a_compiled_lineage(toy_run): + _, declaration, _, _, graph, _ = toy_run + derived = Node( + "toy.central.pension", + "simulate.rules_by_ref@1", + population="toy.central", + inputs=(Slice("person", ("interest_income",)),), + outputs=(Owned("person", "public_pension_income", "float64"),), + ) + reader = Node( + "toy.central.pension.read", + "toy.money_consumer@1", + population="toy.central", + inputs=(Slice("person", ("public_pension_income",)),), + ) + graph = replace(graph, nodes=(*graph.nodes, derived, reader)) + spec = {"country": "xx", "resources": {"money_normalization": declaration}} + compose_module._check_single_money_normalization(spec, graph, compile_graph(graph)) + + +def test_each_compiled_producer_must_match_its_declared_route(toy_run): + population, declaration, context, pin, graph, _ = toy_run + qmap = Node( + "toy.v1.qmap", + "transport.quantile_map@1", + population="toy.v1", + inputs=(Slice("person", ("employment_income",)),), + outputs=(Owned("person", "employment_income", "float64", rewrite=True),), + ) + spec = {"country": "xx", "resources": {"money_normalization": declaration}} + graph_with_map = type(graph)(graph.country, graph.sources, (*graph.nodes, qmap)) + compose_module._check_single_money_normalization( + spec, graph_with_map, compile_graph(graph_with_map) + ) + wrong = copy.deepcopy(declaration) + wrong["quantile_mapped"]["person"].pop("employment_income") + wrong["pending"]["person"]["employment_income"] = { + "awaits": ["x"], + "reason": "x", + } + with pytest.raises(ValueError, match="declared pending but"): + compose_module._check_single_money_normalization( + {"country": "xx", "resources": {"money_normalization": wrong}}, + graph_with_map, + compile_graph(graph_with_map), + ) + + +def test_a_toy_graph_without_a_donor_period_refuses_at_the_normalizer(tmp_path, feed): + population = write_population_sources( + tmp_path / "population", edit_tables=add_money_columns + ) + path, pin, _, _, context = feed + register_transport_codecs(SOURCE_CODECS) + graph = money_graph(population, graph_declaration(), context, pin) + with pytest.raises(Exception, match="records no money period"): + run_graph( + compile_graph(graph), + sources=money_sources(population, path), + store=ContentStore(tmp_path / "store"), + kernels=money_registry(), + ) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_transport_population_kernels.py b/packages/microcosm-build/tests/engine_free/shared/test_transport_population_kernels.py index a6600315f2..90165c090c 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_transport_population_kernels.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_transport_population_kernels.py @@ -136,11 +136,12 @@ def test_kernel_registration_is_explicit_idempotent_and_country_neutral(): ) -def test_aggregate_population_registration_installs_exactly_ten_kernels(): +def test_aggregate_population_registration_installs_exactly_eleven_kernels(): registry = KernelRegistry() assert register_transport_population_kernels(registry) is registry assert register_transport_population_kernels(registry) is registry assert set(registry.refs()) == { + "money.normalize@1", "transport.create@1", "transport.boundary@1", "transport.currency@1", @@ -857,3 +858,23 @@ def test_create_artifacts_pin_python_string_storage_with_pyarrow_installed(tmp_p frame = run.manifest.population(node) for entity, column in text: assert frame.table(entity)[column].dtype == pinned, (node, column) + + +@pytest.mark.parametrize("time_period", [2024, None]) +def test_create_records_the_donor_money_period_in_metadata_and_receipt( + tmp_path, time_period +): + sources = write_population_sources(tmp_path / "sources", time_period=time_period) + node = create_node(sources) + context = KernelContext( + node=node, + tables={}, + weights={}, + strata=pd.Series([], dtype=object), + params=node.params, + rng=np.random.default_rng(0), + sources={"donor": sources.donor, "facts": sources.facts}, + ) + result = TRANSPORT_CREATE.run(context) + assert result.frame.metadata["donor_time_period"] == time_period + assert result.receipt["donor_time_period"] == time_period diff --git a/packages/microcosm-build/tests/golden/nz_country_spec.json b/packages/microcosm-build/tests/golden/nz_country_spec.json index e92e5777f4..7a3a499475 100644 --- a/packages/microcosm-build/tests/golden/nz_country_spec.json +++ b/packages/microcosm-build/tests/golden/nz_country_spec.json @@ -1,6 +1,6 @@ { "country": "nz", - "fingerprint": "ee205064f341d7c2b51bbe9909b516f76e9b0b67991406337fc1f2adbd863470", + "fingerprint": "f03ce44d2e22c3efe18ffe34f99a09dfd9288fafbc6f186b5ad374820a8fd78f", "gate_ids": [ "calibration_per_family_fit", "demography_per_family_fit", @@ -45,14 +45,15 @@ "benefit_unit_rule.json": "a9e8cbfd38a64d2f8db0002fe8c31bb5a18fb34dbe59fa943336487ce95e2ca8", "calibration_bindings.json": "2293672e396407a24caaa5788638e7a1e7fc962575bc2551f500fbc309c3fe32", "chronicle_feed.json": "60a80dd0042838b0412baf9dd0985c961b4253a6876d16bcdbb8a2c6c03de0c7", - "country_package.json": "2e8428292b229c6801f813b5b44d7b3a17a0593c8315b381e05959da7f67630d", - "currency_bridge.json": "aa6a4b286a6a1a015088f32ac013148553422c3d45eeef7d7a15af08388857ca", + "country_package.json": "8a327baa911d48b7da5067918643aa1caff5567e907d4125e73948385351315e", + "currency_bridge.json": "c72732cf36fa60938a6950e4543b6adfbc98b823572c2a6acc517cbe34d20bad", "export_contract.json": "2c7414fefe395c4f5822643dcd3bb3544ebb2040259aa007b828a99bd01ae210", "gates.json": "fa08b508ce1c93ef7627ca06f3a9e808f8fa6ea6f23ce24fa54c58241bbb9f7f", "geography_spine.json": "799bb85e0e4ef943b9f14c6223e2b6168d587e09d1fbf8c7ea2e1aaa0eaee17d", "holdout_references.json": "52b5a2360502156da1392a68350e4e7f61b5fd420fa74428819b65886a9f8655", + "money_normalization.json": "d0fcf0039389e8ef0356227eea83c9a2bd282b34a25a6dade52e2325e2dc67f9", "precal_references.json": "420361e424405df2312bd21808d6b6f4c3428b6fe90c44c37ab156ce197aa25d", - "reference_aging.json": "4987106611ce14b227e483016b384f6c8103e28311c60384df6adbdfba4ca75d", + "reference_aging.json": "e7619ec7a02ae5f460a46d160e89c9c9836c8b1d03140aed2348761e590504f0", "reference_intervals.json": "fcc88c0efd5f416aa15a7d5afbeee1d103dd78a136e13b1efc3973d203188412", "reference_inventory.json": "d16ac4e95f5427c7bb1382ff6ae90e93d4b48344d810755fcc1dc6514430244a", "reference_profiles.json": "cbdcb3bc9fe9c64f0f9280b40996545a8b20ec3a3d173fbdc1080c683715e6d5", @@ -87,6 +88,7 @@ "gates.json", "geography_spine.json", "holdout_references.json", + "money_normalization.json", "precal_references.json", "reference_aging.json", "reference_intervals.json", diff --git a/packages/microcosm-frame/src/microcosm/frame/transport.py b/packages/microcosm-frame/src/microcosm/frame/transport.py index 8353dfa07f..e55fa54746 100644 --- a/packages/microcosm-frame/src/microcosm/frame/transport.py +++ b/packages/microcosm-frame/src/microcosm/frame/transport.py @@ -472,6 +472,8 @@ class DonorBank: The household table carries ``source_household_id`` only when explicit source year and raw household lineage are available. Its typed JSON key merges clones across support channels and distinguishes source vintages. + ``time_period`` is the donor file's recorded model ``_time_period`` year, + or ``None`` when absent. It does not authenticate the amounts' money basis. """ tables: Mapping[str, pd.DataFrame] @@ -482,6 +484,7 @@ class DonorBank: donor_country: str = "us" currency: str = "USD" dropped_parent_cycle_edges: int = 0 + time_period: int | None = None #: Line numbers and pointers are validated as float64 (pointer columns can hold @@ -785,6 +788,19 @@ def _drop_two_person_parent_cycles(person: pd.DataFrame) -> int: return int(drop.sum()) +def _donor_time_period(store: pd.HDFStore) -> int | None: + """The exporter's one-row ``_time_period`` series, or ``None`` when absent.""" + if "/_time_period" not in store.keys(): + return None + values = store["_time_period"] + if not isinstance(values, pd.Series) or len(values) != 1: + raise ValueError("Donor _time_period must hold exactly one value.") + value = values.iloc[0] + if isinstance(value, (bool, np.bool_)) or not isinstance(value, (int, np.integer)): + raise ValueError("Donor _time_period must be an integer year.") + return int(value) + + def read_populace_us_donor(path: str | Path, *, sha256: str, size: int) -> DonorBank: """Authenticate a local donor H5, then decode its person/household inputs. @@ -824,7 +840,9 @@ def read_populace_us_donor(path: str | Path, *, sha256: str, size: int) -> Donor Donor-only enrichment is staged before ``decode``; all work after that seam reads concept names. The result declares transport content with a - US donor and unchanged source-currency amounts and design weights. + US donor and unchanged source-currency amounts and design weights. The + exporter's ``_time_period`` (a one-row integer series) is returned as + ``time_period``; a file without it reads as ``None``. """ path = Path(path) if isinstance(size, bool) or not isinstance(size, int) or size < 0: @@ -845,6 +863,7 @@ def read_populace_us_donor(path: str | Path, *, sha256: str, size: int) -> Donor entity: read_frame_table(store, entity) for entity in ("person", "household") } + time_period = _donor_time_period(store) staged = _donor_extras(raw["person"], raw["household"]) tables = POLICYENGINE_US_CONCEPT_MAPPING.decode(raw) del raw @@ -869,6 +888,7 @@ def read_populace_us_donor(path: str | Path, *, sha256: str, size: int) -> Donor staged["support_strata"], staged["source_person_ids"], dropped_parent_cycle_edges=dropped, + time_period=time_period, ) diff --git a/packages/microcosm-frame/tests/engine_free/shared/test_transport_donor.py b/packages/microcosm-frame/tests/engine_free/shared/test_transport_donor.py index 71257bbddb..15751153d2 100644 --- a/packages/microcosm-frame/tests/engine_free/shared/test_transport_donor.py +++ b/packages/microcosm-frame/tests/engine_free/shared/test_transport_donor.py @@ -77,6 +77,9 @@ def __exit__(self, *_): def __getitem__(self, key): return tables[key].copy(deep=True) + def keys(self): + return [f"/{name}" for name in tables] + def get_storer(self, _): return SimpleNamespace(attrs=SimpleNamespace()) @@ -132,6 +135,7 @@ def spy_decode(mapping, tables): assert donor.donor_country == "us" assert donor.currency == "USD" assert donor.dropped_parent_cycle_edges == 0 + assert donor.time_period is None assert validate_concept_tables(donor.tables) == () assert "A_LINENO" not in person and "bank_account_assets" not in person assert "household_weight" not in donor.tables["household"] @@ -197,6 +201,36 @@ def test_synthetic_donor_transport_and_mapping_round_trip(tmp_path): np.testing.assert_array_equal(donor.weights, original_weights) +@pytest.mark.parametrize("year", [2024, 2023]) +def test_reader_returns_the_exporters_time_period(tmp_path, year): + # The US exporter writes a one-row table-format integer series under + # ``/_time_period`` (adapters/policyengine_us.py); the reader returns it. + path = tmp_path / "dated.h5" + _write_h5(path, _tables()) + with pd.HDFStore(path, mode="a") as store: + store.put("_time_period", pd.Series([year]), format="table") + donor = read_populace_us_donor(path, **_pin(path)) + assert donor.time_period == year and type(donor.time_period) is int + + +@pytest.mark.parametrize( + "series", + [ + pd.Series([2024, 2025]), + pd.Series(["2024"]), + pd.Series([2024.5]), + pd.Series([True]), + ], +) +def test_reader_refuses_a_malformed_time_period(tmp_path, series): + path = tmp_path / "malformed.h5" + _write_h5(path, _tables()) + with pd.HDFStore(path, mode="a") as store: + store.put("_time_period", series, format="table") + with pytest.raises(ValueError, match="_time_period"): + read_populace_us_donor(path, **_pin(path)) + + @pytest.mark.parametrize("mismatch", ["size", "sha256"]) def test_reader_refuses_pin_mismatch_before_hdf_parse(tmp_path, monkeypatch, mismatch): path = tmp_path / "not-an-hdf.h5" diff --git a/test_support/microcosm_build/money_normalization.py b/test_support/microcosm_build/money_normalization.py new file mode 100644 index 0000000000..f87c4194ff --- /dev/null +++ b/test_support/microcosm_build/money_normalization.py @@ -0,0 +1,592 @@ +"""Engine-free fixtures for exact donor money normalization. + +Everything here is synthetic: the destination currency is ``XTS`` (the ISO +4217 code reserved for testing), the toy feed holds invented half-year pay +levels for calendar 2024 and 2027, and the three toy factors are chosen so +their growth roots are exact (``1 + g = q**2`` for ``q`` = 1.01, 1.02 and +1.03). The toy graph reads the frame package's six invented US support +records; none of it describes a country. +""" + +from __future__ import annotations + +import json +from copy import deepcopy +from fractions import Fraction +from pathlib import Path + +import numpy as np + +from microcosm.build.ledger_artifact import ( + chronicle_feed_identity, + load_ledger_consumer_artifact, +) +from microcosm.build.reference_factors import resolve_reference_factor +from microcosm.build.transport.money_kernels import register_money_kernels +from microcosm.graph import ( + Capabilities, + Determinism, + Graph, + KernelBase, + KernelResult, + Node, + Owned, + Slice, + SourceRef, + StructuralDelta, + source_hash, +) +from microcosm.graph.canonical import canonical_json +from test_support.microcosm_build.chronicle_feed import write_pinned_artifact +from test_support.microcosm_build.transport_population import ( + PopulationSources, + create_node, + population_registry, +) + +PROFILES = ("low", "central", "high") +DIMENSION = "toy_index" +#: (entity, column) of every money concept the toy declaration normalizes. +NORMALIZED = { + "person": [ + "nonfarm_self_employment_income", + "farm_self_employment_income", + "interest_income", + "dividend_income", + "rental_income", + "realized_capital_gains", + "private_pension_income", + ], + "household": ["mortgage_interest", "mortgage_principal", "property_tax"], +} +FAMILIES = ("mixed_income", "capital_income", "pensions", "owner_housing_costs") +#: The money columns the synthetic donor carries once ``add_money_columns`` +#: adds two signed raw columns; the graph declaration normalizes these. +GRAPH_NORMALIZED = { + "person": ["interest_income", "rental_income", "farm_self_employment_income"] +} +_GRAPH_ABSENT = { + "person": [ + "nonfarm_self_employment_income", + "dividend_income", + "realized_capital_gains", + "private_pension_income", + ], + "household": ["mortgage_interest", "mortgage_principal", "property_tax"], +} +#: Raw donor rows (decoded by the frame package's US concept mapping) that add +#: signed rental and farm amounts, including a half-cent tie and a negative. +RAW_RENTAL = [-120.5, 0.0, 0.0, 35.25, 0.0, 10.0] +RAW_FARM = [0.0, -2.675, 0.0, 0.0, 0.0, 1000000.0] +_GROWTH = "4.04" +_DELTAS = {"low": "-2.03", "central": "0", "high": "2.05"} +_SOURCE_LEVELS = ("100", "102") +_ANCHOR = "110" +_VALIDATION = { + "unit": "money_per_week", + "source_sha256": "a" * 64, + "source_vintage": "reviewed", + "entity_role": "employee", + "provenance_class": "survey_aggregate", + "assertion": "observation", +} + + +def factor_id(profile: str) -> str: + return f"toy_donor_{profile}" + + +def oracle_factor(profile: str) -> Fraction: + """The rounded toy factor from first principles (exact roots, 6 dp).""" + q = {"low": Fraction(101, 100), "central": Fraction(102, 100)}.get( + profile, Fraction(103, 100) + ) + numerator = Fraction(_ANCHOR) * (1 + q) / 2 + denominator = sum(Fraction(level) for level in _SOURCE_LEVELS) / 2 + return Fraction(round(numerator / denominator * 10**6), 10**6) + + +def toy_declaration( + *, + rate: str = "1.25", + quote: str = "to_per_from", + identity_families: tuple[str, ...] = (), +) -> dict: + """A complete toy declaration; callers edit copies for negative cases.""" + families = { + family: ( + {"identity": True, "reason": "Toy family left unaged on purpose."} + if family in identity_families + else { + "factors": {profile: factor_id(profile) for profile in PROFILES}, + "disclosure": f"Toy {family} aged by the toy pay index.", + } + ) + for family in FAMILIES + } + return { + "schema_version": 1, + "from_currency": "USD", + "to_currency": "XTS", + "rate": { + "form": "decimal_text", + "text": rate, + "citation": { + "publisher": "Toy office", + "title": "Invented yearly average rate", + "url": "https://example.invalid/toy-rate", + }, + }, + "quote": quote, + "rounding": {"digits": 2, "mode": "half_even"}, + "donor_money_period": { + "type": "calendar_year", + "value": 2024, + "basis": "declared_convention", + "evidence": ["Synthetic fixture convention; no donor authentication."], + }, + "columns": deepcopy(NORMALIZED), + "index_family_factors": families, + "factor_profile": { + "dimension": DIMENSION, + "profiles": list(PROFILES), + "central_profile": "central", + }, + "cents_suffix": "_cents", + "quantile_mapped": { + "person": { + "employment_income": { + "awaits": ["toy wage bands"], + "reason": "Toy earnings map onto invented bands.", + }, + "liquid_financial_assets": { + "awaits": ["toy asset bands"], + "reason": "Toy assets map onto invented bands.", + }, + }, + "household": { + "rent": { + "awaits": ["toy rent bands"], + "reason": "Toy rents map onto invented bands.", + } + }, + }, + "pending": {}, + } + + +def graph_declaration() -> dict: + """The toy declaration restricted to the columns the synthetic donor has. + + Money concepts the donor lacks are declared pending, so the roster stays + complete; nothing in the toy graph reads them. + """ + declaration = toy_declaration() + declaration["columns"] = deepcopy(GRAPH_NORMALIZED) + declaration["index_family_factors"] = { + family: declaration["index_family_factors"][family] + for family in ("capital_income", "mixed_income") + } + declaration["pending"] = { + entity: { + column: { + "awaits": ["a donor that carries it"], + "reason": "The synthetic donor has no such column.", + } + for column in columns + } + for entity, columns in _GRAPH_ABSENT.items() + } + return declaration + + +def add_money_columns(tables) -> None: + """``edit_tables`` hook: add the signed raw rental and farm columns.""" + tables["person"]["rental_income"] = RAW_RENTAL + tables["person"]["farm_operations_income"] = RAW_FARM + + +def _slot(period: str, start: str, end: str, sequence: int, *, target: bool) -> dict: + slot = { + "selector": { + "source_name": "toy_office", + "source_measure_id": "weekly_pay", + "entity_name": "person", + "period_type": "half", + "period_value": period, + "assertion": "observation", + "dimensions": [], + }, + "validation": { + **_VALIDATION, + "period_coverage": {"start_date": start, "end_date": end}, + }, + "weight": "1", + "sequence": sequence, + } + if target: + slot["projection_offset"] = sequence + return slot + + +def _fact(key: str, value: str, period: str, start: str, end: str) -> dict: + return { + "aggregate_fact_key": key, + "value": float(value), + "assertion": "observation", + "provenance_class": "survey_aggregate", + "period": {"type": "half", "value": period}, + "period_coverage": {"start_date": start, "end_date": end}, + "entity": {"name": "person", "role": "employee"}, + "dimensions": {}, + "source": { + "source_name": "toy_office", + "source_sha256": "a" * 64, + "vintage": "reviewed", + }, + "observed_measure": { + "source_measure_id": "weekly_pay", + "unit": "money_per_week", + }, + } + + +_SOURCE = ( + ("2024-H1", "2024-01-01", "2024-06-30"), + ("2024-H2", "2024-07-01", "2024-12-31"), +) +_TARGET = ( + ("2027-H1", "2027-01-01", "2027-06-30"), + ("2027-H2", "2027-07-01", "2027-12-31"), +) + + +def money_feed_facts(*, rate: str | None = None) -> list[dict]: + """The toy feed: two 2024 source halves, one 2027 anchor and a growth row.""" + facts = [ + _fact(f"source-{index}", level, *_SOURCE[index]) + for index, level in enumerate(_SOURCE_LEVELS) + ] + facts.append(_fact("anchor-0", _ANCHOR, *_TARGET[0])) + facts.append( + { + "aggregate_fact_key": "growth", + "value": float(_GROWTH), + "assertion": "source_projection", + "provenance_class": "model_output", + "period": {"type": "year", "value": "2027"}, + "period_coverage": {"start_date": "2027-01-01", "end_date": "2027-12-31"}, + "entity": {"name": "person", "role": "employee"}, + "dimensions": {}, + "source": { + "source_name": "toy_office", + "source_sha256": "a" * 64, + "vintage": "reviewed", + }, + "observed_measure": {"source_measure_id": "growth", "unit": "percent"}, + "layout": {"source_column_id": "annual_projection"}, + } + ) + if rate is not None: + facts.append( + { + "aggregate_fact_key": "toy-rate", + "value": float(rate), + "assertion": "observation", + "provenance_class": "administrative", + "period": {"type": "year", "value": "2024"}, + "period_coverage": { + "start_date": "2024-01-01", + "end_date": "2024-12-31", + }, + "entity": {"name": "currency", "role": "rate"}, + "dimensions": {}, + "source": { + "source_name": "toy_bank", + "source_sha256": "b" * 64, + "vintage": "reviewed", + }, + "observed_measure": { + "source_measure_id": "toy_rate", + "unit": "xts_per_usd", + }, + } + ) + return facts + + +def rate_feed_form(identity: dict, expected: str) -> dict: + """A feed-selected rate declaration matching :func:`money_feed_facts`.""" + return { + "form": "feed_selector", + "selector": {"aggregate_fact_key": "toy-rate", "period_value": "2024"}, + "validation": { + "unit": "xts_per_usd", + "source_sha256": "b" * 64, + "source_vintage": "reviewed", + "entity_role": "rate", + "provenance_class": "administrative", + "assertion": "observation", + "period_coverage": {"start_date": "2024-01-01", "end_date": "2024-12-31"}, + }, + "assertion_policy": "observed_only", + "feed_identity": identity, + "expected_decimal": expected, + } + + +def money_factor_declaration( + profile: str, + identity: dict, + *, + purpose: str = "money_normalization_input", + source=_SOURCE, +) -> dict: + numerator = oracle_factor(profile) * 10**6 + return { + "schema_version": 1, + "id": factor_id(profile), + "operation": "projected_period_average_ratio", + "operation_version": 1, + "purpose": purpose, + "feed_identity": deepcopy(identity), + "source_period": "toy-2024", + "target_period": 2027, + "source_schedule": [ + _slot(*row, index, target=False) for index, row in enumerate(source) + ], + "target_schedule": [ + _slot(*row, index, target=True) for index, row in enumerate(_TARGET) + ], + "growth": { + "selector": {"aggregate_fact_key": "growth", "period_value": "2027"}, + "validation": { + **_VALIDATION, + "unit": "percent", + "assertion": "source_projection", + "provenance_class": "model_output", + "period_coverage": { + "start_date": "2027-01-01", + "end_date": "2027-12-31", + }, + "source_column_id": "annual_projection", + }, + "unit_multiplier": "0.01", + "scenario_delta": _DELTAS[profile], + "sensitivity_provenance": ["toy reviewed sensitivity"], + "assertion_policy": "allow_source_projection", + }, + "subperiods_per_year": 2, + "projection_policy": "observed_preferred_latest_contiguous", + "rounding": {"digits": 6, "mode": "nearest_half_even"}, + "expected_operational_factor": { + "numerator": int(numerator), + "denominator": 10**6, + }, + "validity": { + "declared_on": "2025-01-01", + "valid_from": "2025-01-01", + "valid_until": "2099-12-31", + "as_of": "2025-01-01", + }, + "evidence": ["toy published pay levels"], + "decision_references": ["toy reviewed decision"], + } + + +def write_money_feed(root: Path, *, rate: str | None = None): + """Write the toy feed; return ``(path, pin, identity, lexical facts)``.""" + path, pin = write_pinned_artifact(root, money_feed_facts(rate=rate)) + pin["source_commit"] = "c" * 40 + artifact = load_ledger_consumer_artifact(path, feed_pin=pin, lexical_floats=True) + return path, pin, chronicle_feed_identity(pin), artifact.facts + + +def money_factor_context(identity: dict, facts, **declaration_options) -> dict: + """Resolve the three toy factors through the public factor API.""" + entries = [] + for profile in PROFILES: + declaration = money_factor_declaration(profile, identity, **declaration_options) + factor, receipt = resolve_reference_factor(declaration, facts, identity) + entries.append( + { + "declaration": declaration, + "operational_factor": factor, + "receipt": receipt, + } + ) + return {"schema_version": 1, "feed_identity": identity, "factors": entries} + + +def text(document) -> str: + return canonical_json(document).decode("utf-8") + + +def digest(document) -> str: + import hashlib + + return hashlib.sha256(canonical_json(document)).hexdigest() + + +def normalize_node( + node_id: str, + population: str, + declaration: dict, + context: dict, + pin: dict, + *, + selection: dict | None = None, +) -> Node: + """A ``money.normalize@1`` node owning exactly the declared outputs.""" + suffix = declaration["cents_suffix"] + columns = declaration["columns"] + return Node( + node_id, + "money.normalize@1", + population=population, + sources=("money_facts",), + inputs=tuple(Slice(entity, tuple(names)) for entity, names in columns.items()), + outputs=tuple( + owned + for entity, names in columns.items() + for name in names + for owned in ( + Owned(entity, name, "float64", rewrite=True), + Owned(entity, f"{name}{suffix}", "int64"), + ) + ), + params={ + "declaration": text(declaration), + "declaration_sha256": digest(declaration), + "factor_context": text(context), + "factor_context_sha256": digest(context), + "profile_selection": text({} if selection is None else selection), + "chronicle_feed_pin": text(pin), + }, + ) + + +class ToyMoneyConsumer(KernelBase): + """A test-only reader: totals of one normalized column and its companion.""" + + ref = "toy.money_consumer@1" + capabilities = Capabilities(Determinism.DETERMINISTIC, dependencies=("pandas",)) + + def implementation_hash(self) -> str: + return source_hash(type(self), dependencies=self.capabilities.dependencies) + + def run(self, context) -> KernelResult: + table = context.tables["person"] + return KernelResult( + receipt={ + "person_id": [int(value) for value in table["person_id"]], + **{ + column: [float(value) for value in table[column]] + for column in GRAPH_NORMALIZED["person"] + }, + **{ + f"{column}_cents": [ + int(value) for value in table[f"{column}_cents"] + ] + for column in GRAPH_NORMALIZED["person"] + }, + } + ) + + +def consumer_node(node_id: str, population: str) -> Node: + columns = GRAPH_NORMALIZED["person"] + return Node( + node_id, + ToyMoneyConsumer.ref, + population=population, + inputs=( + Slice("person", (*columns, *(f"{column}_cents" for column in columns))), + ), + ) + + +def money_graph( + population: PopulationSources, + declaration: dict, + context: dict, + pin: dict, + *, + variant_selection: dict | None = None, + normalize_open: bool = False, +) -> Graph: + """CREATE -> open -> {central, v1, v2} with one normalization per lineage. + + ``normalize_open`` adds a normalization in the shared open version, so + each branch's lineage normalizes twice. + """ + create = create_node(population) + open_ = Node( + "toy.open", + "transport.boundary@1", + structural=StructuralDelta.FILTER, + base=create.id, + inputs=(Slice("person", ("age",)),), + ) + nodes = [create, open_] + if normalize_open: + nodes.append( + normalize_node("toy.open.normalize", open_.id, declaration, context, pin) + ) + for branch, selection in ( + ("toy.central", None), + ( + "toy.v1", + {DIMENSION: "low"} if variant_selection is None else variant_selection, + ), + ("toy.v2", {DIMENSION: "high"}), + ): + nodes.append( + Node( + branch, + "transport.boundary@1", + structural=StructuralDelta.FILTER, + base=open_.id, + inputs=(Slice("person", ("age",)),), + ) + ) + nodes.append( + normalize_node( + f"{branch}.normalize", + branch, + declaration, + context, + pin, + selection=selection, + ) + ) + nodes.append(consumer_node(f"{branch}.consume", branch)) + return Graph( + "synthetic-money", + ( + SourceRef("donor", "populace-us-h5-v1"), + SourceRef("facts", "ledger-consumer-artifact-v1"), + SourceRef("money_facts", "ledger-consumer-artifact-v1"), + ), + tuple(nodes), + ) + + +def money_registry(): + registry = population_registry() + register_money_kernels(registry) + registry.register(ToyMoneyConsumer()) + return registry + + +def as_float_list(values) -> list[float]: + return [float(value) for value in np.asarray(values, dtype=np.float64)] + + +def dump(document) -> str: + return json.dumps(document, sort_keys=True) + + +def money_sources(population: PopulationSources, feed: Path) -> dict[str, Path]: + """The three sources :func:`money_graph` declares.""" + return {"donor": population.donor, "facts": population.facts, "money_facts": feed} diff --git a/test_support/microcosm_build/transport_population.py b/test_support/microcosm_build/transport_population.py index 60a48bd026..cd6ae806e6 100644 --- a/test_support/microcosm_build/transport_population.py +++ b/test_support/microcosm_build/transport_population.py @@ -116,11 +116,14 @@ def write_population_sources( permutation=None, band_entity: str = "person", edit_tables=None, + time_period: int | None = None, ) -> PopulationSources: """Write the synthetic donor H5 and toy facts. ``edit_tables`` may change the raw donor tables (a dict of DataFrames) in - place before they are written, for a test-local donor. + place before they are written, for a test-local donor. ``time_period`` + writes the exporter's one-row ``_time_period`` table; by default the + donor records none. """ root.mkdir(parents=True, exist_ok=True) document = json.loads(SYNTHETIC_DONOR.read_text()) @@ -135,6 +138,8 @@ def write_population_sources( with pd.HDFStore(donor, mode="w") as store: for entity, table in tables.items(): store.put(entity, table, format="table") + if time_period is not None: + store.put("_time_period", pd.Series([time_period]), format="table") return PopulationSources( donor=donor, facts=write_facts( diff --git a/tools/generate_nz_reference_aging.py b/tools/generate_nz_reference_aging.py index 1a4ce04d12..9030534aa6 100644 --- a/tools/generate_nz_reference_aging.py +++ b/tools/generate_nz_reference_aging.py @@ -1,8 +1,10 @@ """Generate NZ's reviewed ageing declarations and authenticated factor receipts. -Run with ``NZ_CHRONICLE_ARTIFACT`` or ``--artifact``. The six operational +Run with ``NZ_CHRONICLE_ARTIFACT`` or ``--artifact``. The nine operational expectations are assertions of the feed calculation, never arithmetic inputs. -The observed-only ratio is diagnostic and is not available to profiles. +The observed-only ratio is diagnostic and is not available to profiles. The +three donor factors age calendar-2024 donor money to tax year 2026-27; only +money normalization may apply them. """ from __future__ import annotations @@ -29,6 +31,9 @@ (12030, "2023-Q3", "f9f4f127d080fe2f06617947", "1528.92"), (12032, "2023-Q4", "fb5938810b7b37ae8534e5f7", "1556.16"), (12034, "2024-Q1", "854e91ec1dd95e396659ad8e", "1563.54"), + (12036, "2024-Q2", "c07fd9ce3137fa20386bafe0", "1586.32"), + (12038, "2024-Q3", "54a0eeb64049ba4c15e397bf", "1595.11"), + (12040, "2024-Q4", "7fc35ba9682ceb04504f2ef9", "1622.73"), (12044, "2025-Q2", "86ce22ba42a6e2a36b889f9b", "1652.72"), (12046, "2025-Q3", "434e7b2736b4b16589034c72", "1661.12"), (12048, "2025-Q4", "bc87e53157eaf53c846879dc", "1681.06"), @@ -37,6 +42,7 @@ ) _TY2024 = ("2023-Q2", "2023-Q3", "2023-Q4", "2024-Q1") _TY2026 = ("2025-Q2", "2025-Q3", "2025-Q4", "2026-Q1") +_CY2024 = ("2024-Q1", "2024-Q2", "2024-Q3", "2024-Q4") _TARGET = ("2026-Q2", "2026-Q3", "2026-Q4", "2027-Q1") _EXPECTED = { "ird_ty2024_to_ty2027_low": 1115056, @@ -46,11 +52,19 @@ "p1_ty2026_to_ty2027_central": 1028580, "p1_ty2026_to_ty2027_high": 1030467, "observed_ty2026_over_ty2024": 1086069, + "donor_cy2024_to_ty2027_low": 1077090, + "donor_cy2024_to_ty2027_central": 1079073, + "donor_cy2024_to_ty2027_high": 1081052, } _DECISIONS = [ "d1599; IRD band ageing factors, 22 October 2026", "G8-r2 hub rulings 3, 23 October 2026, sections 1–2", ] +_DONOR_DECISIONS = [ + "d1952 card; NZ money year build item: donor money is calendar-2024 dollars", + "G8-w hub rulings 5, 24 October 2026, items H1–H3", + "G8-r2 hub rulings 3, 23 October 2026, section 1", +] def _coverage(period): @@ -199,6 +213,35 @@ def schedule(periods, *, target=False): } ) declarations.append(diagnostic) + for profile in ("low", "central", "high"): + donor = copy.deepcopy( + next( + row + for row in declarations + if row["id"] == f"ird_ty2024_to_ty2027_{profile}" + ) + ) + factor_id = f"donor_cy2024_to_ty2027_{profile}" + donor.update( + { + "id": factor_id, + "purpose": "money_normalization_input", + "source_period": "CY2024", + "source_schedule": schedule(_CY2024), + "expected_operational_factor": { + "numerator": _EXPECTED[factor_id], + "denominator": 1000000, + }, + "evidence": [ + *donor["evidence"], + "Declared calendar-2024 money convention (H1), covering " + "flows and asset stocks; the recorded _time_period is a " + "consistency check, not authentication", + ], + "decision_references": list(_DONOR_DECISIONS), + } + ) + declarations.append(donor) return feed_pin, declarations From b675b7a19319318def74c234f5e324be7bd59dc7 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 24 Oct 2026 22:35:51 -0400 Subject: [PATCH 09/24] Fix the w2a tests that broke on the r2b rebase The hub's validation after rebasing onto #1394 found three failures the lane's pre-rebase runs could not see. - money_normalization.json cited local files by line number, which test_nz_spec_package refuses. The donor-period evidence now names the symbols (the PERIOD constant, _dataset_from_frame, PolicyEngineUSEngine._write_and_verify). A new test parses both files and checks each cited fact against the code. Mutants that change PERIOD, pass a literal time_period, or drop the int cast each fail it. - The rulespec-pin plant edited currency_bridge.json's source record, which w2a moved into money_normalization.json. The plant now edits the IRS rate's citation there, so the planted revision is caught again. - The v2 receipt digests were produced before #1394 added the interval document. The test now passes reference_intervals.json and pins the digests that #1394's 75c27688 produces; they are unchanged at f7000820 and at w2a. The country-spec golden was regenerated for the new money_normalization.json bytes. Co-Authored-By: Claude Opus 5.5 --- .../build/nz/money_normalization.json | 4 +- .../shared/test_nz_money_normalization.py | 67 ++++++++++++++++++- .../shared/test_nz_spec_package.py | 3 +- .../shared/test_reference_profiles.py | 12 ++-- .../tests/golden/nz_country_spec.json | 4 +- 5 files changed, 77 insertions(+), 13 deletions(-) diff --git a/packages/microcosm-build/src/microcosm/build/nz/money_normalization.json b/packages/microcosm-build/src/microcosm/build/nz/money_normalization.json index 2b244bbb7f..0c796e16ee 100644 --- a/packages/microcosm-build/src/microcosm/build/nz/money_normalization.json +++ b/packages/microcosm-build/src/microcosm/build/nz/money_normalization.json @@ -31,8 +31,8 @@ "value": 2024, "basis": "declared_convention", "evidence": [ - "tools/build_us_fiscal_refresh_release.py:410 declares PERIOD = 2024; lines 4320-4328 pass PERIOD as time_period to USSingleYearDataset.", - "packages/microcosm-frame/src/microcosm/frame/adapters/policyengine_us.py:1230-1234 writes the supplied model period as a one-row integer _time_period table. This is model-period lineage evidence, not authentication of the monetary basis of donor amounts." + "tools/build_us_fiscal_refresh_release.py declares the module constant PERIOD = 2024, and its _dataset_from_frame passes PERIOD as time_period to USSingleYearDataset.", + "microcosm.frame.adapters.policyengine_us.PolicyEngineUSEngine._write_and_verify writes the supplied model period as a one-row integer _time_period table. This is model-period lineage evidence, not authentication of the monetary basis of donor amounts." ] }, "columns": { diff --git a/packages/microcosm-build/tests/engine_free/shared/test_nz_money_normalization.py b/packages/microcosm-build/tests/engine_free/shared/test_nz_money_normalization.py index 81a098e79d..da646c4909 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_nz_money_normalization.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_nz_money_normalization.py @@ -7,6 +7,7 @@ from __future__ import annotations +import ast import json from fractions import Fraction @@ -133,9 +134,13 @@ def test_every_family_uses_the_donor_factor_for_its_profile_with_a_disclosure(): def test_donor_money_basis_cites_model_lineage_as_a_declared_convention(): declaration = _declaration() evidence = " ".join(declaration["donor_money_period"]["evidence"]) - assert "tools/build_us_fiscal_refresh_release.py:410" in evidence - assert "4320-4328" in evidence and "time_period" in evidence - assert "frame/adapters/policyengine_us.py:1230-1234" in evidence + assert "tools/build_us_fiscal_refresh_release.py" in evidence + assert "PERIOD = 2024" in evidence and "_dataset_from_frame" in evidence + assert "time_period to USSingleYearDataset" in evidence + assert ( + "microcosm.frame.adapters.policyengine_us.PolicyEngineUSEngine" + "._write_and_verify" in evidence + ) assert "_time_period" in evidence assert "not authentication" in evidence disclosure = declaration["notes"] @@ -144,6 +149,62 @@ def test_donor_money_basis_cites_model_lineage_as_a_declared_convention(): assert "No donor file is opened" in disclosure +def test_donor_money_basis_evidence_matches_the_code_it_cites(): + # The evidence names symbols, not lines (test_nz_spec_package refuses local + # line citations), so each cited fact is checked against the code itself. + repository = paths_for("microcosm-build").repository + tool = ast.parse( + (repository / "tools/build_us_fiscal_refresh_release.py").read_text() + ) + periods = [ + node.value.value + for node in tool.body + if isinstance(node, ast.Assign) + and [getattr(target, "id", None) for target in node.targets] == ["PERIOD"] + ] + assert periods == [_declaration()["donor_money_period"]["value"]] + (builder,) = ( + node + for node in tool.body + if isinstance(node, ast.FunctionDef) and node.name == "_dataset_from_frame" + ) + passed = [ + ast.unparse(keyword.value) + for node in ast.walk(builder) + if isinstance(node, ast.Call) + and getattr(node.func, "id", None) == "USSingleYearDataset" + for keyword in node.keywords + if keyword.arg == "time_period" + ] + assert passed == ["PERIOD"] + adapter = ast.parse( + ( + paths_for("microcosm-frame").package + / "src/microcosm/frame/adapters/policyengine_us.py" + ).read_text() + ) + (engine,) = ( + node + for node in adapter.body + if isinstance(node, ast.ClassDef) and node.name == "PolicyEngineUSEngine" + ) + (writer,) = ( + node + for node in engine.body + if isinstance(node, ast.FunctionDef) and node.name == "_write_and_verify" + ) + stored = [ + ast.unparse(node.args[1]) + for node in ast.walk(writer) + if isinstance(node, ast.Call) + and getattr(node.func, "attr", None) == "put" + and len(node.args) >= 2 + and isinstance(node.args[0], ast.Constant) + and node.args[0].value == "_time_period" + ] + assert stored == ["pd.Series([int(period)])"] + + def test_scenario_knobs_select_their_own_donor_factor(): declaration = _declaration() scenarios = _load("scenarios.json") diff --git a/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py b/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py index 7cd78076ce..d8ed8bcfe3 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py @@ -1620,7 +1620,8 @@ def test_scanner_catches_the_phrasings_the_reviews_planted(self) -> None: surface["notes"] = surface["notes"].replace( "git diff 6fe181fc 8dc2507", "git diff 6fe181fc c0ffee1" ) - payloads["currency_bridge.json"]["source"]["rules"] = { + # The IRS rate's source record lives in money_normalization.json (w2a). + payloads["money_normalization.json"]["rate"]["citation"]["rules"] = { "repository": "TheAxiomFoundation/rulespec-nz.git", "revision": "facade9876543", } diff --git a/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py b/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py index f80863809e..ad562855ab 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py @@ -918,27 +918,29 @@ def test_legacy_receipt_remains_version_one_without_factor_fields(): @pytest.mark.parametrize( ("selection", "digest"), [ - ({}, "e0e36d480868e0d45cb1862e21b00753ba55f5d8500d8fa4c1b1fdcf442395e0"), + ({}, "09fb37b862d2daaa24f62ad2a1660f794885f201f25157bcf7db885fbd521d38"), ( {"aging_index": "low"}, - "406d07423c76eddbbc08601b5bd43e9f84b6b60831d5b18fb22edf3121de03c6", + "50e9554eef9624e5e0d6ae3f3e8e28c1c01925150010db0e58df7f8a2def8254", ), ( {"aging_index": "high"}, - "bcf613d7d3ea17c063cfd944bf6df132c9526700d3da2679d5e943113a3e83c8", + "ebb6f0955d8865f6e8b445db74bd0f8143853852d25bd88374495d1d923c126e", ), ], ) def test_nz_factor_receipts_are_byte_identical_after_additive_factors( selection, digest ): - # Canonical v2 receipts produced at d28817ff, before the donor factors and - # the money_normalization_input purpose were added to the factor document. + # Canonical v2 receipts, with the interval document, produced at #1394's + # 75c27688, before the donor factors and the money_normalization_input + # purpose were added to the factor document. spec = paths_for("microcosm-build").package / "src/microcosm/build/nz" _, _, receipt = resolve_reference_profiles( json.loads((spec / "target_references.json").read_text()), json.loads((spec / "reference_profiles.json").read_text()), selection, + json.loads((spec / "reference_intervals.json").read_text()), factor_context=json.loads((spec / "reference_aging.json").read_text()), ) assert receipt["resolver_version"] == "reference-profiles-v2" diff --git a/packages/microcosm-build/tests/golden/nz_country_spec.json b/packages/microcosm-build/tests/golden/nz_country_spec.json index 7a3a499475..fe209600f6 100644 --- a/packages/microcosm-build/tests/golden/nz_country_spec.json +++ b/packages/microcosm-build/tests/golden/nz_country_spec.json @@ -1,6 +1,6 @@ { "country": "nz", - "fingerprint": "f03ce44d2e22c3efe18ffe34f99a09dfd9288fafbc6f186b5ad374820a8fd78f", + "fingerprint": "af730e2958126da00d4282a96e57d48129ee2c8a68ffe651842a9bdb6b8c7d01", "gate_ids": [ "calibration_per_family_fit", "demography_per_family_fit", @@ -51,7 +51,7 @@ "gates.json": "fa08b508ce1c93ef7627ca06f3a9e808f8fa6ea6f23ce24fa54c58241bbb9f7f", "geography_spine.json": "799bb85e0e4ef943b9f14c6223e2b6168d587e09d1fbf8c7ea2e1aaa0eaee17d", "holdout_references.json": "52b5a2360502156da1392a68350e4e7f61b5fd420fa74428819b65886a9f8655", - "money_normalization.json": "d0fcf0039389e8ef0356227eea83c9a2bd282b34a25a6dade52e2325e2dc67f9", + "money_normalization.json": "fc7d095ede90c32901d3bf59c21d8e4ec52f7a56e6a37a908721c230160f4dda", "precal_references.json": "420361e424405df2312bd21808d6b6f4c3428b6fe90c44c37ab156ce197aa25d", "reference_aging.json": "e7619ec7a02ae5f460a46d160e89c9c9836c8b1d03140aed2348761e590504f0", "reference_intervals.json": "fcc88c0efd5f416aa15a7d5afbeee1d103dd78a136e13b1efc3973d203188412", From 4f333f2c2ee70b6b758586f402d966df7297a2f0 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 24 Oct 2026 23:22:51 -0400 Subject: [PATCH 10/24] Declare the money target period and refuse normalizing dropped concepts From the #1395 round-1 review (findings m1, m4 and n4; m2 in part). - A money declaration now names the target_period its factors age money to, as it already names the donor period. check_money_factor refuses a factor whose target_period differs, so no two families can age money to different periods. The declaration's sha256 already binds it into the kernel receipt. NZ declares 2027 (tax year 2026-27), the target of its donor factors and of every reference-profile factor. - A concept dropped in transport can't be listed as normalized; it used to validate and only fail at run time. Quantile-mapped and pending rows may still name one. - NZ's scenarios' default aging_index must be the money declaration's central profile, and the two must list the same profiles. - The mixed-income disclosure now says the projection after June 2026 uses Treasury PREFU 2026 AWE growth (2.7%, 0.5 points lower and higher in the low and high profiles), not only Stats NZ QES. The general two-step rounding property is renamed for what it asserts: at most floor(1 + f/2) cents, so one cent for every factor below 2. In-memory mutants that drop each new guard (the DROP refusal, the target-period check, the target-period validation) each fail their test. The NZ country-spec golden is regenerated for the new declaration bytes. Co-Authored-By: Claude Opus 5.5 --- changelog.d/nz-money-normalization.added.md | 6 ++- .../microcosm/build/money_normalization.py | 30 ++++++++++-- .../build/nz/money_normalization.json | 3 +- .../shared/test_money_normalization.py | 48 ++++++++++++++++++- .../shared/test_nz_money_normalization.py | 22 +++++++++ .../tests/golden/nz_country_spec.json | 4 +- .../microcosm_build/money_normalization.py | 1 + 7 files changed, 104 insertions(+), 10 deletions(-) diff --git a/changelog.d/nz-money-normalization.added.md b/changelog.d/nz-money-normalization.added.md index 2422d3bd94..1e8381d4a2 100644 --- a/changelog.d/nz-money-normalization.added.md +++ b/changelog.d/nz-money-normalization.added.md @@ -1,7 +1,9 @@ Add exact, declared donor money normalization: a pure module that validates a complete money-normalization declaration (cited decimal-text or feed-replayed -rate, explicit quote, half-even rounding, donor money period, per-family factors -or explicit identities, and quantile-mapped or pending rosters) and applies it +rate, explicit quote, half-even rounding, donor money period, the one target +period every factor must age money to, per-family factors or explicit +identities, and quantile-mapped or pending rosters; a concept dropped in +transport can't be normalized) and applies it with exact rationals, and a `money.normalize@1` transport kernel that replays its factors against the authenticated feed, refuses a donor money period that differs from the one CREATE records, and adds an integer cents companion per diff --git a/packages/microcosm-build/src/microcosm/build/money_normalization.py b/packages/microcosm-build/src/microcosm/build/money_normalization.py index 7828e59a0f..280a38c3f8 100644 --- a/packages/microcosm-build/src/microcosm/build/money_normalization.py +++ b/packages/microcosm-build/src/microcosm/build/money_normalization.py @@ -26,7 +26,8 @@ Every normalized column maps through its concept's index family to one declared factor, or to an explicit identity with a reason; nothing falls back -to a factor of one. Every money concept that survives transport is declared +to a factor of one. Every applied factor ages money to the one declared +``target_period``. Every money concept that survives transport is declared exactly once: normalized, quantile mapped or pending. """ @@ -91,6 +92,7 @@ "quote", "rounding", "donor_money_period", + "target_period", "columns", "index_family_factors", "factor_profile", @@ -253,7 +255,14 @@ def _columns(value: object) -> dict[str, list[str]]: "Money normalization columns must be nonempty lists of distinct names." ) for name in names: - _money_concept(entity, name, "column") + item = _money_concept(entity, name, "column") + if item.transport is TransportRule.DROP: + # Quantile maps and pending rows may still name a dropped + # concept; a normalizer would only fail later, at run time. + raise ValueError( + f"Money normalization column {entity}.{name} is dropped in " + "transport, so it cannot be normalized." + ) result[entity] = list(names) return result @@ -378,6 +387,11 @@ def validate_money_normalization(document: object) -> dict: if rounding["mode"] not in _ROUNDING_MODES: raise ValueError("Money normalization rounding mode must be half_even.") row["donor_money_period"] = _period(row["donor_money_period"]) + if type(row["target_period"]) is not int or not 1 <= row["target_period"] <= 9999: + raise ValueError( + "Money normalization target_period must be the integer period label " + "its factors age money to." + ) row["columns"] = _columns(row["columns"]) if ( not isinstance(row["cents_suffix"], str) @@ -513,8 +527,10 @@ def selected_money_factors( def check_money_factor(entry: Mapping, declaration: Mapping) -> tuple[int, int]: """Return ``(p, k)`` for a factor ``p / 10**k`` fit for this declaration. - The factor must carry the money-normalization purpose, and its source - schedule must cover exactly the declared donor money period. + The factor must carry the money-normalization purpose, its source + schedule must cover exactly the declared donor money period, and it must + age money to the declared ``target_period``, so every family reaches the + same destination period. """ factor_declaration = entry["declaration"] if factor_declaration["purpose"] != MONEY_FACTOR_PURPOSE: @@ -523,6 +539,12 @@ def check_money_factor(entry: Mapping, declaration: Mapping) -> tuple[int, int]: f"{factor_declaration['purpose']!r}; money normalization applies only " f"{MONEY_FACTOR_PURPOSE} factors." ) + target = factor_declaration["target_period"] + if type(target) is not int or target != declaration["target_period"]: + raise ValueError( + f"Factor {factor_declaration['id']!r} ages money to {target!r}, not " + f"the declared target_period {declaration['target_period']!r}." + ) schedule = factor_declaration["source_schedule"] coverage = ( schedule[0]["validation"]["period_coverage"]["start_date"], diff --git a/packages/microcosm-build/src/microcosm/build/nz/money_normalization.json b/packages/microcosm-build/src/microcosm/build/nz/money_normalization.json index 0c796e16ee..e6c6c2be96 100644 --- a/packages/microcosm-build/src/microcosm/build/nz/money_normalization.json +++ b/packages/microcosm-build/src/microcosm/build/nz/money_normalization.json @@ -35,6 +35,7 @@ "microcosm.frame.adapters.policyengine_us.PolicyEngineUSEngine._write_and_verify writes the supplied model period as a one-row integer _time_period table. This is model-period lineage evidence, not authentication of the monetary basis of donor amounts." ] }, + "target_period": 2027, "columns": { "person": [ "nonfarm_self_employment_income", @@ -67,7 +68,7 @@ "central": "donor_cy2024_to_ty2027_central", "high": "donor_cy2024_to_ty2027_high" }, - "disclosure": "Self-employment income (both columns, signed) is aged by Stats NZ QES ordinary-time average weekly earnings growth, the AWE convention; provisional pending Max's ruling on non-wage ageing (H3). No separate mixed-income index is used." + "disclosure": "Self-employment income (both columns, signed) is aged by ordinary-time average weekly earnings (AWE) growth, the AWE convention: Stats NZ QES observations through June 2026, then Treasury PREFU 2026 forecast AWE growth of 2.7% (0.5 points lower and higher in the low and high profiles); provisional pending Max's ruling on non-wage ageing (H3). No separate mixed-income index is used." }, "capital_income": { "factors": { diff --git a/packages/microcosm-build/tests/engine_free/shared/test_money_normalization.py b/packages/microcosm-build/tests/engine_free/shared/test_money_normalization.py index f8d3acd787..6595197f29 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_money_normalization.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_money_normalization.py @@ -56,6 +56,7 @@ "quote", "rounding", "donor_money_period", + "target_period", "columns", "index_family_factors", "factor_profile", @@ -155,6 +156,28 @@ def test_toy_declaration_validates_and_rosters_partition_every_money_concept(): } +def test_a_dropped_concept_cannot_be_normalized(): + declaration = toy_declaration() + declaration["columns"]["person"].append("public_pension_income") + with pytest.raises(ValueError, match="public_pension_income is dropped in"): + validate_money_normalization(declaration) + # A pending row may still name a dropped concept. + declaration = toy_declaration() + declaration["pending"] = { + "person": {"public_pension_income": {"awaits": ["test"], "reason": "test"}} + } + rosters = money_column_rosters(validate_money_normalization(declaration)) + assert rosters["person", "public_pension_income"] == ("pending", ("test",)) + + +@pytest.mark.parametrize("value", ["2027", 2027.0, True, None, 0, 10000]) +def test_the_target_period_is_an_integer_period_label(value): + declaration = toy_declaration() + declaration["target_period"] = value + with pytest.raises(ValueError, match="target_period must be the integer period"): + validate_money_normalization(declaration) + + @pytest.mark.parametrize("field", _FIELDS) def test_every_field_is_required(field): declaration = toy_declaration() @@ -673,6 +696,28 @@ def test_money_factors_carry_their_purpose_and_cover_the_donor_period(tmp_path): check_money_factor(context["factors"][1], other) +@pytest.mark.parametrize("target", [2026, 2028]) +def test_every_factor_ages_money_to_the_declared_target_period(tmp_path, target): + # Each family's factor is checked against the one declared destination, + # so no two families can age to different periods. + _, _, identity, facts = write_money_feed(tmp_path / "feed") + declaration = validate_money_normalization(toy_declaration()) + context = money_factor_context(identity, facts) + other = copy.deepcopy(declaration) + other["target_period"] = target + for entry in context["factors"]: + assert entry["declaration"]["target_period"] == declaration["target_period"] + check_money_factor(entry, declaration) + with pytest.raises( + ValueError, match=f"not the declared target_period {target}" + ): + check_money_factor(entry, other) + stray = copy.deepcopy(entry) + stray["declaration"]["target_period"] = target + with pytest.raises(ValueError, match=f"ages money to {target}, not"): + check_money_factor(stray, declaration) + + # --------------------------------------------------------------------------- # Properties against the independent oracle # --------------------------------------------------------------------------- @@ -759,9 +804,10 @@ def test_permuting_rows_commutes_with_normalization(values, seed, factor): factor=_FACTORS, quote=st.sampled_from(["to_per_from", "from_per_to"]), ) -def test_two_step_rounding_differs_from_one_step_by_at_most_one_cent( +def test_two_step_rounding_stays_within_the_factor_bound_of_one_step( value, rate, factor, quote ): + # At most floor(1 + f/2) minor units, so one cent for every factor below 2. exact_factor = Fraction(factor, 10**6) _, two, one = _oracle(value, Fraction(rate), quote, exact_factor) assert abs(two - one) <= math.floor(1 + exact_factor / 2) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_nz_money_normalization.py b/packages/microcosm-build/tests/engine_free/shared/test_nz_money_normalization.py index da646c4909..a1d9185555 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_nz_money_normalization.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_nz_money_normalization.py @@ -216,6 +216,28 @@ def test_scenario_knobs_select_their_own_donor_factor(): assert set(factors.values()) == {f"donor_cy2024_to_ty2027_{expected}"} +def test_the_scenarios_default_aging_index_is_the_money_central_profile(): + # A scenario that leaves the knob unset selects the declaration's central + # profile, so the scenarios' default must name the same profile. + profile = _declaration()["factor_profile"] + scenarios = _load("scenarios.json") + dimension = profile["dimension"] + assert scenarios["central_knobs"][dimension] == profile["central_profile"] + assert set(scenarios["knob_domains"][dimension]) == set(profile["profiles"]) + + +def test_money_and_the_reference_profiles_age_to_one_target_period(): + declaration = _declaration() + factors = [ + entry["declaration"] for entry in _load("reference_aging.json")["factors"] + ] + donor = {row["id"]: row["target_period"] for row in factors if row["id"] in _DONOR} + assert donor == dict.fromkeys(_DONOR, declaration["target_period"]) + assert { + row["target_period"] for row in factors if row["purpose"] == "profile_input" + } == {declaration["target_period"]} + + @pytest.mark.parametrize("profile", ["low", "central", "high"]) def test_a_missing_declared_profile_factor_refuses(profile): declaration = _load("money_normalization.json") diff --git a/packages/microcosm-build/tests/golden/nz_country_spec.json b/packages/microcosm-build/tests/golden/nz_country_spec.json index fe209600f6..49e83ea2df 100644 --- a/packages/microcosm-build/tests/golden/nz_country_spec.json +++ b/packages/microcosm-build/tests/golden/nz_country_spec.json @@ -1,6 +1,6 @@ { "country": "nz", - "fingerprint": "af730e2958126da00d4282a96e57d48129ee2c8a68ffe651842a9bdb6b8c7d01", + "fingerprint": "27c04ffa8c4240306dc13f446a1d679436e649fe8d35f5cab4370bbb5e92968c", "gate_ids": [ "calibration_per_family_fit", "demography_per_family_fit", @@ -51,7 +51,7 @@ "gates.json": "fa08b508ce1c93ef7627ca06f3a9e808f8fa6ea6f23ce24fa54c58241bbb9f7f", "geography_spine.json": "799bb85e0e4ef943b9f14c6223e2b6168d587e09d1fbf8c7ea2e1aaa0eaee17d", "holdout_references.json": "52b5a2360502156da1392a68350e4e7f61b5fd420fa74428819b65886a9f8655", - "money_normalization.json": "fc7d095ede90c32901d3bf59c21d8e4ec52f7a56e6a37a908721c230160f4dda", + "money_normalization.json": "799540e5ea5004a39210c60df199b8d15e6b185b3aa68f0daabf44435779aeb6", "precal_references.json": "420361e424405df2312bd21808d6b6f4c3428b6fe90c44c37ab156ce197aa25d", "reference_aging.json": "e7619ec7a02ae5f460a46d160e89c9c9836c8b1d03140aed2348761e590504f0", "reference_intervals.json": "fcc88c0efd5f416aa15a7d5afbeee1d103dd78a136e13b1efc3973d203188412", diff --git a/test_support/microcosm_build/money_normalization.py b/test_support/microcosm_build/money_normalization.py index f87c4194ff..cf11cca876 100644 --- a/test_support/microcosm_build/money_normalization.py +++ b/test_support/microcosm_build/money_normalization.py @@ -145,6 +145,7 @@ def toy_declaration( "basis": "declared_convention", "evidence": ["Synthetic fixture convention; no donor authentication."], }, + "target_period": 2027, "columns": deepcopy(NORMALIZED), "index_family_factors": families, "factor_profile": { From 495db830eb4f3684b46adc75a07a8a4627722487 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 25 Oct 2026 10:54:02 -0400 Subject: [PATCH 11/24] Check factor currency in preparation and bind the warm-store test to a factor prepare_transport_build now runs the run-entry currency check itself, after it validates the resume policy and before activation. Every library caller that starts a new run (auto or forbid) therefore refuses an expired selected factor, not only the CLI; require still reaches stored work. main drops its own call, and execute_transport_graph stays clock-free: its docstring says a caller that composes a graph itself owns the check. New tests cover library callers under auto and forbid for the default and scenario views, the require exemption, and resume validation before the currency check. The warm-store replay test now compiles a factor-bound target surface. A test-local targets.compile@1 node on nz.open reads the factor references and the pinned factor feed, and is a CLI terminal endpoint, so the warm run executes it. The test spies the factor replay and checks it ran once against the facts of the pinned feed. After the factor expires, require runs no kernel or replay, keeps the export bytes and serves the compiler from the store under the same key; auto refuses on the factor before preparation work. Co-Authored-By: GPT-6.1 Sol Co-Authored-By: Claude Opus 5.5 --- .../src/microcosm/build/transport/cli.py | 18 +- .../shared/test_transport_composed_graph.py | 189 ++++++++++++++++-- 2 files changed, 182 insertions(+), 25 deletions(-) diff --git a/packages/microcosm-build/src/microcosm/build/transport/cli.py b/packages/microcosm-build/src/microcosm/build/transport/cli.py index 38411e7af6..6f42b3b0f8 100644 --- a/packages/microcosm-build/src/microcosm/build/transport/cli.py +++ b/packages/microcosm-build/src/microcosm/build/transport/cli.py @@ -230,7 +230,12 @@ def prepare_transport_build( inventory_store: ContentStore | str | Path | None = None, resume: ResumePolicy = "auto", ) -> PreparedTransportBuild: - """Check activation, build the registry and CREATE inventory, then compose. + """Check factor currency and activation, then compose the prepared graph. + + A new run (``auto`` or ``forbid``) first requires every factor the default + and scenario views select to be current on today's UTC date; ``require`` + only verifies stored work. This is the run-entry check for every caller, + the CLI included. Composition and execution stay clock-free. Before registry preparation, CREATE or graph source reads, activation checks declaration resource paths, non-null selected roots and encoding @@ -248,9 +253,10 @@ def prepare_transport_build( CREATE sources and reuses the stored inventory without running the probe. """ - validate_transport_activation(spec) if resume not in {"auto", "require", "forbid"}: raise ValueError("resume must be 'auto', 'require' or 'forbid'.") + _require_current_run_factors(spec, resume=resume) + validate_transport_activation(spec) if resume == "require": root = ( inventory_store.root @@ -473,6 +479,9 @@ def execute_transport_graph( later checkpoints reuse this invocation's work. The skeleton H5 is rewritten only when the export descriptor changed; an unchanged H5 must pass readback, and under ``require`` it must already exist. + + Execution is clock-free. It runs a graph that :func:`prepare_transport_build` + checked; a caller that composes a graph itself owns the currency check. """ if resume not in {"auto", "require", "forbid"}: @@ -666,8 +675,8 @@ def _require_current_run_factors( def main(argv: Sequence[str] | None = None) -> int: """Run the CLI; return 0 on success and 1 on a refused build. - The currency check guards new runs only (``auto`` and ``forbid``), before - preparation can run CREATE. ``require`` verifies existing stored work. + Preparation's currency check guards new runs only (``auto`` and + ``forbid``), before CREATE. ``require`` verifies existing stored work. """ try: @@ -689,7 +698,6 @@ def main(argv: Sequence[str] | None = None) -> int: "--resume require refuses a cold transport graph store." ) spec = load_transport_spec(args.spec_dir if args.spec_dir else args.country) - _require_current_run_factors(spec, resume=args.resume) prepared = prepare_transport_build( spec, sources=sources, inventory_store=store_root, resume=args.resume ) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_transport_composed_graph.py b/packages/microcosm-build/tests/engine_free/shared/test_transport_composed_graph.py index 4187358f15..0e67af8374 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_transport_composed_graph.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_transport_composed_graph.py @@ -52,8 +52,10 @@ from hypothesis import HealthCheck, given, settings from hypothesis import strategies as st +from microcosm.build.reference_profiles import reference_document_sha256 from microcosm.build.transport import cli as cli_module from microcosm.build.transport import compose as compose_module +from microcosm.build.transport import target_kernels as target_kernels_module from microcosm.build.transport.artifact_types import COMPARISON_TYPE from microcosm.build.transport.cli import ( EXPORT_FILENAME, @@ -1665,10 +1667,7 @@ def test_cli_main_refuses_expired_factors_before_create( ) monkeypatch.setattr(cli_module, "_today", lambda: _AFTER_FACTOR_EXPIRY) monkeypatch.setattr(cli_module, "load_transport_spec", lambda source: spec) - prepare = Mock(side_effect=AssertionError("Must refuse before preparation.")) - create = Mock(side_effect=AssertionError("Must refuse before CREATE.")) - monkeypatch.setattr(cli_module, "prepare_transport_build", prepare) - monkeypatch.setattr(cli_module.TRANSPORT_CREATE, "run", create) + later = _mock_steps_after_the_currency_check(monkeypatch) out = tmp_path / "out" assert ( cli_module.main(["--country", "xx", "--out", str(out), "--resume", resume]) == 1 @@ -1676,11 +1675,68 @@ def test_cli_main_refuses_expired_factors_before_create( error = capsys.readouterr().err assert "toy_factor" in error assert "reviewed re-declaration" in error - prepare.assert_not_called() - create.assert_not_called() + for step in later: + step.assert_not_called() assert not out.exists() +def _mock_steps_after_the_currency_check(monkeypatch): + """Activation, the registry, the CREATE inventory and CREATE, each refusing.""" + steps = [] + for owner, name in ( + (cli_module, "validate_transport_activation"), + (cli_module, "build_transport_registry"), + (cli_module, "prepare_create_outputs"), + (cli_module.TRANSPORT_CREATE, "run"), + ): + step = Mock(side_effect=AssertionError(f"Must refuse before {name}.")) + monkeypatch.setattr(owner, name, step) + steps.append(step) + return steps + + +@pytest.mark.parametrize("resume", ["auto", "forbid"]) +@pytest.mark.parametrize("selection", ["default", "scenario"]) +def test_preparation_refuses_expired_factors_for_library_callers( + tmp_path, monkeypatch, resume, selection +): + # prepare_transport_build is exported: a caller that skips main must meet + # the same run-entry check, before activation, the registry or CREATE. + _, _, _, _, spec = factor_transport_fixture(tmp_path) + if selection == "scenario": + spec["resources"]["reference_profiles"]["dimensions"]["index"]["profiles"][ + "central" + ]["transformations"] = [] + spec["resources"]["scenarios"]["scenarios"].append( + {"id": "variant", "knobs": {"index": "low"}} + ) + monkeypatch.setattr(cli_module, "_today", lambda: _AFTER_FACTOR_EXPIRY) + later = _mock_steps_after_the_currency_check(monkeypatch) + with pytest.raises(ValueError, match="toy_factor") as error: + cli_module.prepare_transport_build(spec, sources={}, resume=resume) + assert "reviewed re-declaration" in str(error.value) + for step in later: + step.assert_not_called() + + +def test_preparation_lets_require_reach_stored_work_after_expiry(tmp_path, monkeypatch): + # require skips the currency check and goes on to activation and the store. + _, _, _, _, spec = factor_transport_fixture(tmp_path) + monkeypatch.setattr(cli_module, "_today", lambda: _AFTER_FACTOR_EXPIRY) + reached = Mock(side_effect=LookupError("activation reached")) + monkeypatch.setattr(cli_module, "validate_transport_activation", reached) + with pytest.raises(LookupError, match="activation reached"): + cli_module.prepare_transport_build(spec, sources={}, resume="require") + reached.assert_called_once_with(spec) + + +def test_preparation_checks_resume_before_currency(tmp_path, monkeypatch): + _, _, _, _, spec = factor_transport_fixture(tmp_path) + monkeypatch.setattr(cli_module, "_today", lambda: _AFTER_FACTOR_EXPIRY) + with pytest.raises(ValueError, match="resume must be"): + cli_module.prepare_transport_build(spec, sources={}, resume="sometimes") + + def test_cli_factor_currency_decision_allows_historical_replay(tmp_path, monkeypatch): # The factor fixture has parked measurement rows and no runnable skeleton. # Exercise the CLI decision directly; the warm-store test below verifies @@ -1782,22 +1838,87 @@ def test_cli_require_replays_a_warm_store_after_selected_factors_expire( ): # End to end: warm a real store while a selected factor is current, then # replay it past valid_until. require verifies and runs nothing; a new run - # of the same spec refuses on that factor before preparation. - _, _, _, _, factors = factor_transport_fixture(tmp_path / "factor") + # of the same spec refuses on that factor before preparation work. + facts, pin, _, _, factors = factor_transport_fixture(tmp_path / "factor") spec = copy.deepcopy(composed.spec) spec["resources"].update( factor_target_references=factors["resources"]["target_references"], factor_context=factors["resources"]["factor_context"], + factor_feed=pin, reference_profiles={ **factors["resources"]["reference_profiles"], "reference_resource": "factor_target_references", }, ) - assert compose_module.reference_profile_view(spec)[2]["selected_factors"] + profile_view = compose_module.reference_profile_view + + def measured_view(spec, selection=None): + # As in factor_transport_fixture(activate=True), supply a synthetic + # reviewed measurement closure. The real resolver still selects and + # binds the factors; composition must attach its rehashed receipt. + # Copies, so the resolver's own return values are never edited. + references, intervals, receipt = profile_view(spec, selection) + references, receipt = copy.deepcopy(references), copy.deepcopy(receipt) + for row in references["target_references"]: + row["metadata"]["activation_status"] = "active" + receipt["effective_references_sha256"] = reference_document_sha256(references) + receipt.pop("receipt_sha256") + receipt["receipt_sha256"] = reference_document_sha256(receipt) + return references, intervals, receipt + + monkeypatch.setattr(compose_module, "reference_profile_view", measured_view) + declaration = spec["resources"]["transport_graph"] + compiler_id = "nz.factor.targets.compile" + declaration["sources"].append( + { + **next( + source + for source in declaration["sources"] + if source["name"] == "nz_calibration_facts" + ), + "name": "factor_facts", + } + ) + declaration["nodes"].append( + { + "id": compiler_id, + "kernel": "targets.compile@1", + "population": "nz.open", + "sources": ["factor_facts"], + "params": { + "country": factors["country"], + "references": { + "resource": "factor_target_references", + "encoding": "json", + }, + "references_sha256": { + "resource": "factor_target_references", + "encoding": "sha256", + }, + "chronicle_feed_pin": {"resource": "factor_feed", "encoding": "json"}, + }, + } + ) + # This factor-bound compiler is an explicit CLI terminal endpoint, so its + # target surface must actually be compiled and cached during the warm run. + declaration["endpoints"]["terminal"].append(compiler_id) + compiler = compose_transport_graph(spec, composed.config).node(compiler_id) + assert compiler.kernel == "targets.compile@1" + assert "reference_resolution" in compiler.params + resolution = json.loads(compiler.params["reference_resolution"]) + assert resolution["selected_factors"] + assert json.loads(compiler.params["chronicle_feed_pin"]) == pin directory = _write_local_transport_spec(tmp_path / "spec", spec) build_registry = cli_module.build_transport_registry - observe_warm = [] calls = [] + replay = Mock( + wraps=target_kernels_module.reference_factors_module.replay_reference_factor + ) + monkeypatch.setattr( + target_kernels_module.reference_factors_module, + "replay_reference_factor", + replay, + ) def injected_registry(bindings, root, *, unit_rule, engines_by_binding=None): registry = build_registry( @@ -1806,39 +1927,67 @@ def injected_registry(bindings, root, *, unit_rule, engines_by_binding=None): unit_rule=unit_rule, engines_by_binding=composed.engines_by_binding, ) - if observe_warm: - for ref in registry.kernels.refs(): - kernel = registry.kernels.get(ref) - run = kernel.run + for ref in registry.kernels.refs(): + kernel = registry.kernels.get(ref) + run = kernel.run - def counted(context, _run=run, _ref=ref): - calls.append(_ref) - return _run(context) + def counted(context, _run=run): + calls.append(context.node.id) + return _run(context) - monkeypatch.setattr(kernel, "run", counted) + monkeypatch.setattr(kernel, "run", counted) return registry monkeypatch.setattr(cli_module, "build_transport_registry", injected_registry) out = tmp_path / "out" argv = ["--spec-dir", str(directory), "--out", str(out)] - for name, path in composed.sources.items(): + for name, path in {**composed.sources, "factor_facts": facts}.items(): argv.extend(["--source", f"{name}={path}"]) monkeypatch.setattr(cli_module, "_today", lambda: date(2050, 1, 1)) assert cli_module.main(argv) == 0 assert capsys.readouterr().err == "" + assert calls.count(compiler_id) == 1 + replay.assert_called_once() + factor = resolution["selected_factors"]["toy_factor"] + # The replay ran against the authenticated factor feed: the facts an + # independent load of the pinned factor_facts source yields. + fed_facts = ( + target_kernels_module.ledger_artifact_module.load_ledger_consumer_artifact( + facts, feed_pin=pin, lexical_floats=True + ).facts + ) + assert fed_facts + replay.assert_called_once_with( + factor["declaration"], + fed_facts, + factor["declaration"]["feed_identity"], + factor["operational_factor"], + factor["receipt"], + ) + warm_node = json.loads((out / "manifest.json").read_bytes())["nodes"][compiler_id] + assert warm_node["receipt"]["n_targets"] == 2 exported = (out / EXPORT_FILENAME).read_bytes() - observe_warm.append(True) + calls.clear() + replay.reset_mock() monkeypatch.setattr(cli_module, "_today", lambda: _AFTER_FACTOR_EXPIRY) assert cli_module.main([*argv, "--resume", "require"]) == 0 assert capsys.readouterr().err == "" # require reads back the stored run's export and rewrites nothing. assert (out / EXPORT_FILENAME).read_bytes() == exported assert calls == [] + replay.assert_not_called() + stored_node = json.loads((out / "manifest.json").read_bytes())["nodes"][compiler_id] + assert stored_node["hit"] is True + assert stored_node["key"] == warm_node["key"] + assert stored_node["opaque_artifacts"] == warm_node["opaque_artifacts"] + later = _mock_steps_after_the_currency_check(monkeypatch) assert cli_module.main([*argv, "--resume", "auto"]) == 1 error = capsys.readouterr().err assert "toy_factor" in error assert "reviewed re-declaration" in error assert calls == [] + for step in later: + step.assert_not_called() def test_cli_main_invalid_arguments_are_a_refusal(capsys): From 2f220f70c39f0c04fc17e1034caf41c2c51f43ed Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 25 Oct 2026 10:56:41 -0400 Subject: [PATCH 12/24] Add receipt contract v2 and selection-aware reference preflight A receipt contract that declares contract_version takes the v2 validator and assignment; one without it runs the v1 bodies unchanged. v2 contracts are closed at contract, profile, program and stratum level, and add: - strata whose filters must be disjoint on the data, each with its own rate and the rest of the population not assigned; - an assignment profile chosen by a knob, giving the draw key suffix and the exclusion order; - exclude_columns with v1's semantics (distinct, non-null booleans), applied after the judgments and payment screen and before group exclusions; - per-group overlap and weighted Hall feasibility diagnostics, which never refuse; a target above its remaining eligible mass still refuses, with the audit attached; - per stratum, the realized weighted count beside the target, and a check that rate times eligible mass reproduces the target to a relative 1e-12. The four reserved unit fields admit only "person". Judgments and exclusions read the entitlement_entity table, payments the payee table and draws the draw_unit table; strata resolve, and flags are written, at projection. Activation checks that the default profile is the central knob's value and that each stratum filter names a calibration_bindings predicate. A v1 node still refuses v2's optional parameters as unknown. Predicate validation and evaluation move to calibration_predicates, shared by calibration preparation and receipt strata, so strata validate their raw inputs rather than trust prepared columns. Reference selection gains opt-in per-resource exemptions (schema_version 1, each naming its activation status and a reason). An exempted placeholder no longer counts as an activation gap. Activation refuses an exempt reference that some node selects; activation and compilation both refuse an unknown or active exemption and a stale declared status, checked against the full effective document before any row is filtered, and a compiled surface refuses to carry an exempt reference. Compose passes a selection only to target compilers whose resource has exemptions, so specs without one compile as before. The new modules join the implementation hashes of takeup.assign, calibration.prepare and the target compilers, so their cache keys change for every country. Co-Authored-By: GPT-6.1 Sol Co-Authored-By: Claude Opus 5.5 --- .../nzhub-receipt-contract-v2.added.md | 6 + .../microcosm/build/calibration_columns.py | 116 +-- .../microcosm/build/calibration_predicates.py | 168 ++++ .../build/transport/column_kernels.py | 130 ++- .../src/microcosm/build/transport/compose.py | 232 ++++- .../build/transport/receipt_contract.py | 546 ++++++++++ .../build/transport/reference_selection.py | 118 +++ .../build/transport/takeup_kernels.py | 20 + .../build/transport/target_kernels.py | 64 +- .../shared/test_calibration_columns.py | 204 +++- .../shared/test_transport_column_kernels.py | 17 + .../shared/test_transport_receipt_v2.py | 944 ++++++++++++++++++ .../test_transport_receipt_v2_kernel.py | 188 ++++ .../test_transport_reference_selection.py | 665 ++++++++++++ .../reference_selection_legacy.py.txt | 239 +++++ 15 files changed, 3545 insertions(+), 112 deletions(-) create mode 100644 changelog.d/nzhub-receipt-contract-v2.added.md create mode 100644 packages/microcosm-build/src/microcosm/build/calibration_predicates.py create mode 100644 packages/microcosm-build/src/microcosm/build/transport/receipt_contract.py create mode 100644 packages/microcosm-build/src/microcosm/build/transport/reference_selection.py create mode 100644 packages/microcosm-build/tests/engine_free/shared/test_transport_receipt_v2.py create mode 100644 packages/microcosm-build/tests/engine_free/shared/test_transport_receipt_v2_kernel.py create mode 100644 packages/microcosm-build/tests/engine_free/shared/test_transport_reference_selection.py create mode 100644 packages/microcosm-build/tests/fixtures/reference_selection_legacy.py.txt diff --git a/changelog.d/nzhub-receipt-contract-v2.added.md b/changelog.d/nzhub-receipt-contract-v2.added.md new file mode 100644 index 0000000000..1b86bc346f --- /dev/null +++ b/changelog.d/nzhub-receipt-contract-v2.added.md @@ -0,0 +1,6 @@ +Add opt-in version-two receipt contracts with validated disjoint strata, +profile-selected draws and exclusion order, reserved declared-unit dispatch, +v1-compatible existing boolean exclusion columns applied before rate calculation, +weighted overlap and joint-feasibility diagnostics, and numerical expectation +checks. Add selection-aware reference preflight and compilation that refuses +explicitly selected exemptions while preserving the legacy path. diff --git a/packages/microcosm-build/src/microcosm/build/calibration_columns.py b/packages/microcosm-build/src/microcosm/build/calibration_columns.py index c7a986b21b..c41ee074d1 100644 --- a/packages/microcosm-build/src/microcosm/build/calibration_columns.py +++ b/packages/microcosm-build/src/microcosm/build/calibration_columns.py @@ -8,14 +8,18 @@ from __future__ import annotations -import math from collections.abc import Mapping -from numbers import Integral, Real from typing import Any import numpy as np import pandas as pd +from microcosm.build.calibration_predicates import ( + _finite_number, + _identity, + predicate_mask, + validate_calibration_inputs, +) from microcosm.build.reference_intervals import ( interval_indicators, validate_interval_document, @@ -37,29 +41,7 @@ def prepare_calibration_columns( and one of its bands; every record of each named table is validated, and any refusal raises, before an output exists. """ - if bindings.get("schema_version") != 1: - raise ValueError("Calibration bindings require schema_version 1.") - required = bindings.get("required_inputs") - domains = bindings.get("input_domains") - columns = bindings.get("columns") - if not isinstance(required, Mapping) or not isinstance(domains, Mapping): - raise ValueError("Calibration bindings need required_inputs and input_domains.") - if not isinstance(columns, (list, tuple)) or not columns: - raise ValueError("Calibration bindings need nonempty columns.") - for entity, names in required.items(): - if entity not in tables: - raise ValueError(f"Calibration preparation needs the {entity!r} table.") - if not isinstance(names, (list, tuple)) or not names: - raise ValueError(f"Calibration required_inputs for {entity!r} are invalid.") - entity_domains = domains.get(entity, {}) - for name in names: - if name not in tables[entity]: - raise ValueError(f"Calibration preparation needs {entity}.{name}.") - domain = entity_domains.get(name) - if not isinstance(domain, Mapping): - raise ValueError(f"Calibration input {entity}.{name} needs a domain.") - _validate_input(tables[entity][name], domain, f"{entity}.{name}") - + required, columns = validate_calibration_inputs(tables, bindings) interval_columns = _interval_columns(tables, columns, required, intervals) out = {entity: table.copy(deep=True) for entity, table in tables.items()} derived = _prepare_roster_counts(out, bindings.get("roster_counts", ()), required) @@ -91,35 +73,13 @@ def prepare_calibration_columns( raise ValueError(f"Calibration constant {entity}.{name} is invalid.") prepared.setdefault(entity, {})[name] = value continue - predicates = entry.get("predicates") - if not isinstance(predicates, (list, tuple)) or not predicates: - raise ValueError(f"Calibration output {entity}.{name} needs predicates.") - mask = np.ones(len(out[entity]), dtype=bool) - for predicate in predicates: - if not isinstance(predicate, Mapping): - raise ValueError("Calibration predicates must be objects.") - column = predicate.get("column") - if ( - column not in {*required[entity], *derived.get(entity, ())} - or "value" not in predicate - ): - raise ValueError( - f"Calibration predicate needs a validated {entity} input." - ) - values = out[entity][column] - operator, value = predicate.get("operator"), predicate["value"] - if operator == "eq": - part = values.eq(value) - elif operator == "ge": - part = values.ge(value) - elif operator == "lt": - part = values.lt(value) - elif operator == "in" and isinstance(value, (list, tuple)) and value: - part = values.isin(value) - else: - raise ValueError(f"Unsupported calibration predicate {operator!r}.") - mask &= part.to_numpy(dtype=bool) - prepared.setdefault(entity, {})[name] = mask + prepared.setdefault(entity, {})[name] = predicate_mask( + out[entity], + entry.get("predicates"), + entity=entity, + name=name, + validated_inputs={*required[entity], *derived.get(entity, ())}, + ) for entity, values in prepared.items(): original_order = list(out[entity].columns) computed = pd.DataFrame(values, index=out[entity].index) @@ -206,25 +166,6 @@ def _interval_columns( return result -def _finite_number(value: object) -> bool: - return ( - isinstance(value, Real) and not isinstance(value, bool) and math.isfinite(value) - ) - - -def _identity(value: object) -> tuple[type, object]: - if ( - isinstance(value, bool) - or not isinstance(value, (str, Integral)) - or isinstance(value, str) - and not value - ): - raise ValueError( - "Calibration membership identities must be strings or integers." - ) - return (str, value) if isinstance(value, str) else (int, int(value)) - - def _prepare_roster_counts( tables: dict[str, pd.DataFrame], entries: object, required: Mapping[str, Any] ) -> dict[str, set[str]]: @@ -278,32 +219,3 @@ def _prepare_roster_counts( tables[entity][name] = [counts[key] for key in groups] derived.setdefault(entity, set()).add(name) return derived - - -def _validate_input(values: pd.Series, domain: Mapping[str, Any], name: str) -> None: - if values.isna().any(): - raise ValueError(f"Calibration input {name} contains missing values.") - if domain.get("kind") == "identity": - for value in values.tolist(): - _identity(value) - valid = True - elif domain.get("kind") == "signed_integer": - valid = all( - isinstance(value, Integral) and not isinstance(value, (bool, np.bool_)) - for value in values.tolist() - ) - elif domain.get("kind") == "nonnegative_integer": - valid = all( - _finite_number(value) and value >= 0 and value == int(value) - for value in values.tolist() - ) - elif isinstance(domain.get("values"), (list, tuple)) and domain["values"]: - allowed = domain["values"] - valid = all( - any(type(value) is type(member) and value == member for member in allowed) - for value in values.tolist() - ) - else: - raise ValueError(f"Calibration input {name} has an unsupported domain.") - if not valid: - raise ValueError(f"Calibration input {name} is outside its declared domain.") diff --git a/packages/microcosm-build/src/microcosm/build/calibration_predicates.py b/packages/microcosm-build/src/microcosm/build/calibration_predicates.py new file mode 100644 index 0000000000..13d3816235 --- /dev/null +++ b/packages/microcosm-build/src/microcosm/build/calibration_predicates.py @@ -0,0 +1,168 @@ +"""Validated calibration predicates shared by preparation and receipt assignment. + +Receipt assignment precedes calibration preparation, so evaluating a predicate +must validate its declared raw inputs rather than trust existing indicator columns. +""" + +from __future__ import annotations + +import math +from collections.abc import Collection, Mapping +from numbers import Integral, Real +from typing import Any + +import numpy as np +import pandas as pd + + +def validate_calibration_inputs( + tables: Mapping[str, pd.DataFrame], bindings: Mapping[str, Any] +) -> tuple[Mapping[str, Any], list[Any] | tuple[Any, ...]]: + """Validate every declared input using preparation's original contract. + + Return the declarations so callers can validate their outputs without repeating + schema or input validation. No table or column is mutated. + """ + if bindings.get("schema_version") != 1: + raise ValueError("Calibration bindings require schema_version 1.") + required = bindings.get("required_inputs") + domains = bindings.get("input_domains") + columns = bindings.get("columns") + if not isinstance(required, Mapping) or not isinstance(domains, Mapping): + raise ValueError("Calibration bindings need required_inputs and input_domains.") + if not isinstance(columns, (list, tuple)) or not columns: + raise ValueError("Calibration bindings need nonempty columns.") + for entity, names in required.items(): + if entity not in tables: + raise ValueError(f"Calibration preparation needs the {entity!r} table.") + if not isinstance(names, (list, tuple)) or not names: + raise ValueError(f"Calibration required_inputs for {entity!r} are invalid.") + entity_domains = domains.get(entity, {}) + for name in names: + if name not in tables[entity]: + raise ValueError(f"Calibration preparation needs {entity}.{name}.") + domain = entity_domains.get(name) + if not isinstance(domain, Mapping): + raise ValueError(f"Calibration input {entity}.{name} needs a domain.") + _validate_input(tables[entity][name], domain, f"{entity}.{name}") + + return required, columns + + +def predicate_mask( + table: pd.DataFrame, + predicates: object, + *, + entity: str, + name: str, + validated_inputs: Collection[str], +) -> np.ndarray: + """Evaluate a conjunction after its caller has validated all named inputs.""" + if not isinstance(predicates, (list, tuple)) or not predicates: + raise ValueError(f"Calibration output {entity}.{name} needs predicates.") + mask = np.ones(len(table), dtype=bool) + for predicate in predicates: + if not isinstance(predicate, Mapping): + raise ValueError("Calibration predicates must be objects.") + column = predicate.get("column") + if column not in validated_inputs or "value" not in predicate: + raise ValueError(f"Calibration predicate needs a validated {entity} input.") + values = table[column] + operator, value = predicate.get("operator"), predicate["value"] + if operator == "eq": + part = values.eq(value) + elif operator == "ge": + part = values.ge(value) + elif operator == "lt": + part = values.lt(value) + elif operator == "in" and isinstance(value, (list, tuple)) and value: + part = values.isin(value) + else: + raise ValueError(f"Unsupported calibration predicate {operator!r}.") + mask &= part.to_numpy(dtype=bool) + return mask + + +def calibration_predicate_mask( + tables: Mapping[str, pd.DataFrame], + bindings: Mapping[str, Any], + *, + entity: str, + name: str, +) -> np.ndarray: + """Resolve one declared predicate filter and validate before evaluating it. + + Receipt strata use raw required inputs only. Roster-derived inputs remain owned + by calibration preparation; neither stale prepared indicators nor unvalidated + table columns can stand in for a declared predicate input. + """ + required, columns = validate_calibration_inputs(tables, bindings) + matches = [ + entry + for entry in columns + if isinstance(entry, Mapping) + and entry.get("entity") == entity + and entry.get("name") == name + ] + if entity not in required or len(matches) != 1: + raise ValueError( + f"Calibration filter {entity}.{name} needs exactly one declared predicate column." + ) + entry = matches[0] + if "constant" in entry or "interval" in entry: + raise ValueError(f"Calibration filter {entity}.{name} must use predicates.") + return predicate_mask( + tables[entity], + entry.get("predicates"), + entity=entity, + name=name, + validated_inputs=required[entity], + ) + + +def _finite_number(value: object) -> bool: + return ( + isinstance(value, Real) and not isinstance(value, bool) and math.isfinite(value) + ) + + +def _identity(value: object) -> tuple[type, object]: + if ( + isinstance(value, bool) + or not isinstance(value, (str, Integral)) + or isinstance(value, str) + and not value + ): + raise ValueError( + "Calibration membership identities must be strings or integers." + ) + return (str, value) if isinstance(value, str) else (int, int(value)) + + +def _validate_input(values: pd.Series, domain: Mapping[str, Any], name: str) -> None: + if values.isna().any(): + raise ValueError(f"Calibration input {name} contains missing values.") + if domain.get("kind") == "identity": + for value in values.tolist(): + _identity(value) + valid = True + elif domain.get("kind") == "signed_integer": + valid = all( + isinstance(value, Integral) and not isinstance(value, (bool, np.bool_)) + for value in values.tolist() + ) + elif domain.get("kind") == "nonnegative_integer": + valid = all( + _finite_number(value) and value >= 0 and value == int(value) + for value in values.tolist() + ) + elif isinstance(domain.get("values"), (list, tuple)) and domain["values"]: + allowed = domain["values"] + valid = all( + any(type(value) is type(member) and value == member for member in allowed) + for value in values.tolist() + ) + else: + raise ValueError(f"Calibration input {name} has an unsupported domain.") + if not valid: + raise ValueError(f"Calibration input {name} is outside its declared domain.") diff --git a/packages/microcosm-build/src/microcosm/build/transport/column_kernels.py b/packages/microcosm-build/src/microcosm/build/transport/column_kernels.py index 576d3cdfd8..bfa7eadf58 100644 --- a/packages/microcosm-build/src/microcosm/build/transport/column_kernels.py +++ b/packages/microcosm-build/src/microcosm/build/transport/column_kernels.py @@ -38,6 +38,7 @@ import numpy as np import pandas as pd +from microcosm.build import calibration_predicates as calibration_predicates_module from microcosm.calibrate import hierarchy as hierarchy_module from microcosm.calibrate import registry as registry_module from microcosm.frame import concept_mapping as mapping_module @@ -73,6 +74,7 @@ from microcosm.graph.population import dtype_for_token from . import graph_inputs +from . import receipt_contract as receipt_contract_module from .artifact_types import TARGET_SURFACE_TYPE from .graph_inputs import ( canonical_document_param, @@ -80,6 +82,12 @@ sha256_param, string_param, ) +from .receipt_contract import ( + assign_receipts_v2, + receipt_dispatch_tables, + receipt_strata, + validate_receipt_v2, +) from .target_kernels import decode_target_surface __all__ = [ @@ -508,6 +516,8 @@ def validate_receipt_contract( column/reference text, distinct exclusion columns, and finite rates in [0, 1]. A null optional payment column remains allowed. """ + if "contract_version" in contract: + return validate_receipt_v2(contract) if set(contract) != {"seed_column", "programs", "exclusion_groups"}: raise ValueError( "Receipt contract declares seed_column, programs and exclusion_groups." @@ -582,6 +592,9 @@ def assign_receipts( *, weights: np.ndarray | None = None, targets: Mapping[str, float] | None = None, + bindings: Mapping[str, object] | None = None, + knobs: Mapping[str, object] | None = None, + tables: Mapping[str, pd.DataFrame] | None = None, ) -> tuple[pd.DataFrame, dict[str, object]]: """Apply explicit receipt priorities to determined eligible program draws. @@ -590,6 +603,16 @@ def assign_receipts( semantics. Existing exclusions are read as booleans and never rewritten. The data-independent contract checks use ``validate_receipt_contract``. """ + if "contract_version" in contract: + return assign_receipts_v2( + person, + contract, + weights=weights, + targets=targets, + bindings=bindings, + knobs=knobs, + tables=tables, + ) seed_column, parsed, groups = validate_receipt_contract(contract) out = person.loc[:, ["person_id"]].copy() audit = {} @@ -682,15 +705,31 @@ def implementation_hash(self) -> str: target_kernels, registry_module, hierarchy_module, + calibration_predicates_module, + receipt_contract_module, dependencies=self.capabilities.dependencies, ) def run(self, context: KernelContext) -> KernelResult: + required = frozenset({"receipt_contract", "receipt_contract_sha256"}) require_params( context, self.ref, - required=frozenset({"receipt_contract", "receipt_contract_sha256"}), + required=required, + optional=frozenset( + { + "calibration_bindings", + "calibration_bindings_sha256", + "profile_selection", + } + ), ) + contract = _document(context, self.ref, "receipt_contract") + if "contract_version" in contract: + return self._run_v2(context, contract) + # v1 takes none of v2's optional parameters; they refuse as unknown, + # exactly as before v2 existed. + require_params(context, self.ref, required=required) tables = _whole_tables( context, self.ref, ("person",), artifact_aliases=frozenset({"surface"}) ) @@ -747,6 +786,95 @@ def run(self, context: KernelContext) -> KernelResult: receipt={"programs": audit}, ) + def _run_v2(self, context, contract): + _, programs, _ = validate_receipt_v2(contract) + entities = tuple( + dict.fromkeys( + row[field] + for row in programs + for field in receipt_contract_module.RECEIPT_UNIT_FIELDS + ) + ) + tables = _whole_tables( + context, + self.ref, + tuple(dict.fromkeys((*entities, *context.tables))), + artifact_aliases=frozenset({"surface"}), + ) + owned = {(item.entity, item.column) for item in context.node.outputs} + expected = {(row["projection"], row["output"]) for row in programs} + unknown = owned - expected + if unknown: + raise ValueError( + f"{self.ref} unmodelled or unknown receipt outputs {sorted(unknown)}." + ) + if owned != expected: + raise ValueError( + f"{self.ref} must own exactly the contract's receipt flags." + ) + if ( + set(context.artifacts) != {"surface"} + or context.artifacts["surface"].type != TARGET_SURFACE_TYPE + ): + raise ValueError(f"{self.ref} v2 reads the typed surface artifact.") + surface = decode_target_surface(context.artifacts["surface"].payload) + targets = {} + for row in programs: + for stratum in receipt_strata(row): + name = stratum["target"] + matches = [ + target for target in surface.registry.specs if target.name == name + ] + if len(matches) != 1: + raise ValueError( + f"Receipt target {name!r} must identify one surface row." + ) + (target,) = matches + if ( + target.entity != row["projection"] + or target.filter != stratum["filter"] + or target.measure != row["output"] + or target.metadata.get("ledger_measure_unit") != "count" + ): + raise ValueError( + f"Receipt target {name!r} must be a {row['projection']} count of {row['output']!r} with declared stratum filter {stratum['filter']!r}." + ) + targets[name] = target.value + bindings = ( + _document(context, self.ref, "calibration_bindings") + if "calibration_bindings" in context.params + else None + ) + knobs = ( + canonical_document_param(context, self.ref, "profile_selection") + if "profile_selection" in context.params + else None + ) + outputs, program_audits = {}, {} + # Dispatch is read from all four reserved fields; validation currently + # permits their person values only, so this loop runs once, over one + # projection entity. It passes the whole contract and keeps the last + # audit, which is right only for one entity; v3 replaces it. + for entity in dict.fromkeys(row["projection"] for row in programs): + entity_rows = [row for row in programs if row["projection"] == entity] + table = receipt_dispatch_tables(tables, entity_rows[0])["projection"] + assigned, audit = assign_receipts( + table, + contract, + weights=context.weights[entity].values + if entity in context.weights + else None, + targets=targets, + bindings=bindings, + knobs=knobs, + tables=tables, + ) + outputs[entity] = assigned + program_audits = audit + return KernelResult( + columns=_columns(context, self.ref, outputs), receipt=program_audits + ) + TRANSPORT_UNIT_ATTRIBUTES = UnitAttributesKernel() CONCEPTS_ENCODE = ConceptsEncodeKernel() diff --git a/packages/microcosm-build/src/microcosm/build/transport/compose.py b/packages/microcosm-build/src/microcosm/build/transport/compose.py index ea7129b29d..a41847478a 100644 --- a/packages/microcosm-build/src/microcosm/build/transport/compose.py +++ b/packages/microcosm-build/src/microcosm/build/transport/compose.py @@ -102,6 +102,7 @@ from .artifact_types import KERNEL_OUTPUTS from .column_kernels import validate_receipt_contract from .money_kernels import MONEY_NORMALIZE_REF +from .reference_selection import selection_resources, validate_exemptions from .target_kernels import TARGET_COMPILATION_KERNELS __all__ = [ @@ -573,16 +574,162 @@ def _null_paths(value: object, path: str = "") -> Iterator[str]: yield from _null_paths(item, f"{path}[{index}]") -def _reference_activation_gaps(document: Mapping, resource: str) -> Iterator[str]: +def _reference_activation_gaps( + document: Mapping, + resource: str, + exempt: Mapping | None = None, + selected: Mapping[str, Sequence[str]] | None = None, +) -> Iterator[str]: + # The absent-resource path preserves the legacy activation walk and messages. + exemptions = ( + set() + if exempt is None + else { + row["name"] + for row in validate_exemptions( + document, exempt, resource=resource, selected=selected + ) + } + ) for row in _sequence( document.get("target_references"), f"{resource}.target_references" ): reference = _mapping(row, f"{resource} reference") metadata = _mapping(reference.get("metadata", {}), f"{resource}.metadata") - if metadata.get("activation_status", "active") != "active": + if ( + reference.get("name") not in exemptions + and metadata.get("activation_status", "active") != "active" + ): yield f"placeholder reference {resource}.{reference.get('name', '?')}" +def _reference_selections(spec: Mapping[str, object]) -> dict[str, dict]: + if not _has_resource(spec, "reference_selection"): + return {} + return selection_resources(transport_resource(spec, "reference_selection")) + + +def _instantiated_rows( + spec: Mapping[str, object], +) -> list[tuple[Mapping, Mapping | None]]: + """Common, scenario and variant rows actually used by the factories.""" + rows = [] + if _has_resource(spec, GRAPH_RESOURCE): + rows.extend( + (row, None) + for row in transport_resource(spec, GRAPH_RESOURCE).get("nodes", ()) + if isinstance(row, Mapping) + ) + if _has_resource(spec, "entitlement_graph") and _has_resource(spec, "scenarios"): + from .entitlement import _rows + + rows.extend(_rows(spec)[0]) + return rows + + +def _preflight_parameter(spec, value, scenario): + """Resolve only data selectors; preparation-dependent bindings stay literal.""" + if isinstance(value, Mapping) and "resource" in value: + selected = _resource_param( + spec, + value, + allow_json_sequences=True, + profile_selection=None if scenario is None else scenario.get("knobs", {}), + ) + return json.loads(selected) if value.get("encoding") == "json" else selected + if isinstance(value, Mapping) and "scenario" in value and scenario is not None: + selected = ( + scenario if value["scenario"] == "row" else scenario[value["scenario"]] + ) + for component in value.get("path", ()): + selected = selected[component] + return selected + if isinstance(value, str): + try: + return json.loads(value) + except json.JSONDecodeError: + return value + return value + + +def _named_reference_values(value: object) -> Iterator[str]: + if isinstance(value, Mapping): + for key, item in value.items(): + if key in {"reference", "target"} and isinstance(item, str): + yield item + else: + yield from _named_reference_values(item) + elif isinstance(value, list | tuple): + for item in value: + yield from _named_reference_values(item) + + +def _reference_selection_closure(spec: Mapping[str, object]) -> dict[str, dict]: + """Map resource/row to selecting node ids, before exemption filtering. + + Reading a complete ``references`` document in a target compiler is not a + row selection. A comparison selects every row it requests. Explicit target + and reference keys, receipt strata, mass rows and annualisation-factor keys + select rows. Artifact edges associate these names with their source + reference resource; unbound names are checked against every matching row. + """ + rows = _instantiated_rows(spec) + by_id = {row.get("id"): row for row, _ in rows} + resources = { + _stem(name): document + for name, document in _resources(spec).items() + if isinstance(document, Mapping) and "target_references" in document + } + closure: dict[str, dict[str, list[str]]] = {name: {} for name in resources} + + def origins(row): + result = set() + for name in ("references", "mass_reference"): + value = row.get("params", {}).get(name) + if isinstance(value, Mapping) and isinstance(value.get("resource"), str): + result.add(_stem(value["resource"])) + for edge in row.get("artifact_inputs", ()): + producer = by_id.get(edge.get("producer"), {}) + value = producer.get("params", {}).get("references") + if isinstance(value, Mapping) and isinstance(value.get("resource"), str): + result.add(_stem(value["resource"])) + return result + + for row, scenario in rows: + params = row.get("params", {}) + names = set() + for key, value in params.items(): + data = _preflight_parameter(spec, value, scenario) + if key in {"reference", "target"} and isinstance(data, str): + names.add(data) + elif key == "annualisation_factors" and isinstance(data, Mapping): + names.update(data) + elif key == "mass_reference" or ( + key == "references" and row.get("kernel") == "takeup.compare@1" + ): + if isinstance(data, Mapping): + names.update( + item["name"] for item in data.get("target_references", ()) + ) + elif key not in {"references", "reference_resolution"}: + names.update(_named_reference_values(data)) + candidates = origins(row) or set(resources) + for resource in candidates: + if resource not in closure: + continue + known = { + item.get("name") for item in resources[resource]["target_references"] + } + for name in names & known: + closure[resource].setdefault(name, []).append(str(row.get("id", "?"))) + # Every mass row is a required population denominator, even if a malformed + # graph omitted its CREATE consumer. Give that requirement a concrete label. + if "mass_references" in resources: + for item in resources["mass_references"]["target_references"]: + closure["mass_references"].setdefault(item["name"], ["mass_references"]) + return closure + + def _declared_rows(spec: Mapping[str, object]) -> list[Mapping]: """Skeleton rows plus the entitlement rows the factory instantiates.""" rows = [] @@ -862,6 +1009,12 @@ def validate_transport_activation(spec: Mapping[str, object]) -> None: missing = [] # Resolve first so activation never inspects an executable profile's raw rows. reference_profile_views(spec) + reference_selections = _reference_selections(spec) + selection_closure = ( + _reference_selection_closure(spec) + if _has_resource(spec, "reference_selection") + else {} + ) selections = ( tuple(_declared_resource_selections(transport_resource(spec, GRAPH_RESOURCE))) if _has_resource(spec, GRAPH_RESOURCE) @@ -927,6 +1080,7 @@ def validate_transport_activation(spec: Mapping[str, object]) -> None: if _has_resource(spec, name) and "target_references" in transport_resource(spec, name) ) + reference_resources.update(reference_selections) checked_reference_views = set() for resource in sorted(reference_resources): if not _has_resource(spec, resource): @@ -935,10 +1089,24 @@ def validate_transport_activation(spec: Mapping[str, object]) -> None: document = transport_resource(spec, resource) identity = hashlib.sha256(canonical_json(document)).hexdigest() checked_reference_views.add((resource, identity)) - missing.extend(_reference_activation_gaps(document, resource)) + missing.extend( + _reference_activation_gaps( + document, + resource, + reference_selections.get(resource), + selection_closure.get(resource), + ) + ) for (resource, identity), document in selected_reference_views.items(): if (resource, identity) not in checked_reference_views: - missing.extend(_reference_activation_gaps(document, resource)) + missing.extend( + _reference_activation_gaps( + document, + resource, + reference_selections.get(resource), + selection_closure.get(resource), + ) + ) receipt = ( transport_resource(spec, "receipt_contract") if _has_resource(spec, "receipt_contract") @@ -954,6 +1122,38 @@ def validate_transport_activation(spec: Mapping[str, object]) -> None: else: try: validate_receipt_contract(contracts) + if contracts.get("contract_version") == 2: + scenarios = transport_resource(spec, "scenarios") + central = _mapping(scenarios.get("central_knobs", {}), "central_knobs") + knob = contracts["assignment_knob"] + if central.get(knob) != contracts["default_assignment_profile"]: + raise ValueError( + "default_assignment_profile must equal " + f"central_knobs[{knob!r}]." + ) + strata_declared = any( + program.get("strata") for program in contracts["programs"] + ) + bindings = ( + transport_resource(spec, "calibration_bindings") + if strata_declared + else {} + ) + filters = { + (entry.get("entity"), entry.get("name")) + for entry in bindings.get("columns", ()) + if isinstance(entry, Mapping) and "predicates" in entry + } + for program in contracts["programs"]: + for stratum in program.get("strata", ()): + if ( + program.get("projection", "person"), + stratum["filter"], + ) not in filters: + raise ValueError( + f"Receipt stratum {stratum['filter']!r} must name " + "calibration_bindings predicates." + ) except (ValueError, TypeError) as error: missing.append(f"receipt_contract.json path ['receipts']: {error}") if _has_resource(spec, "axiom_rules_bindings"): @@ -1520,6 +1720,30 @@ def _node( } ) params = _target_feed_params(spec, kernel, params) + raw_params = _mapping(row.get("params", {}), "node params") + raw_references = raw_params.get("references") + if ( + kernel in TARGET_COMPILATION_KERNELS + and isinstance(raw_references, Mapping) + and isinstance(raw_references.get("resource"), str) + ): + selection = _reference_selections(spec).get(_stem(raw_references["resource"])) + if selection is not None and selection["exempt"]: + params["reference_selection"] = canonical_json(selection).decode("utf-8") + if kernel == "takeup.assign@1" and "receipt_contract" in params: + receipt_contract = json.loads(params["receipt_contract"]) + if receipt_contract.get("contract_version") == 2: + if _has_resource(spec, "calibration_bindings"): + params["calibration_bindings"] = canonical_json( + transport_resource(spec, "calibration_bindings") + ).decode("utf-8") + params["calibration_bindings_sha256"] = _resource_digest( + spec, "calibration_bindings" + ) + central = transport_resource(spec, "scenarios").get("central_knobs", {}) + params["profile_selection"] = canonical_json( + {**central, **(profile_selection or {})} + ).decode("utf-8") if ( kernel in TARGET_COMPILATION_KERNELS | {"calibration.prepare@1"} and "references" in params diff --git a/packages/microcosm-build/src/microcosm/build/transport/receipt_contract.py b/packages/microcosm-build/src/microcosm/build/transport/receipt_contract.py new file mode 100644 index 0000000000..9227d8803c --- /dev/null +++ b/packages/microcosm-build/src/microcosm/build/transport/receipt_contract.py @@ -0,0 +1,546 @@ +"""Version-two receipt validation and assignment, independent of country packs.""" + +from __future__ import annotations + +import itertools +import json +import math +from collections.abc import Mapping + +import numpy as np + +from microcosm.build.calibration_predicates import calibration_predicate_mask +from microcosm.frame.concepts import derive_take_up_draws +from microcosm.frame.input_closure import UndeterminedAction, resolve_judgments + +RECEIPT_RELATIVE_TOLERANCE = 1 / 10**12 +RECEIPT_UNIT_FIELDS = ("entitlement_entity", "payee", "draw_unit", "projection") + + +class ReceiptAssignmentRefused(ValueError): # noqa: N818 + """An assignment refusal retaining its diagnostic result, including order.""" + + def __init__(self, message, audit): + super().__init__(message) + self.audit = audit + + +def _text(value, name): + if not isinstance(value, str) or not value: + raise ValueError(f"Receipt {name} must be non-empty text.") + return value + + +def _strings(values, name, *, nonempty=False): + if not isinstance(values, list | tuple) or (nonempty and not values): + raise ValueError( + f"Receipt {name} must be a {'non-empty ' if nonempty else ''}list." + ) + result = [_text(value, name) for value in values] + if len(set(result)) != len(result): + raise ValueError(f"Receipt {name} must contain unique names.") + return result + + +def receipt_draw_key(program, suffix): + """A tuple encoding: even adversarial delimiters cannot alias another pair.""" + return json.dumps([program, suffix], ensure_ascii=True, separators=(",", ":")) + + +def reversed_receipt_groups(groups, order): + if order not in {"declared", "reversed"}: + raise ValueError("Receipt exclusion_order must be declared or reversed.") + return [ + list(group) if order == "declared" else list(reversed(group)) + for group in groups + ] + + +def receipt_dispatch_tables(tables, row): + """Dispatch through each declared unit, ready for the declared-unit extension.""" + # [BE] Reserved fields drive the declared-unit receipt extension's dispatch. + return {field: tables[row[field]] for field in RECEIPT_UNIT_FIELDS} + + +def validate_receipt_v2(contract): + required = { + "contract_version", + "seed_column", + "default_assignment_profile", + "assignment_knob", + "assignment_profiles", + "programs", + "exclusion_groups", + } + if not required <= set(contract) or set(contract) - required - { + "unmodelled_programs" + }: + raise ValueError("Receipt v2 contract has missing or unknown fields.") + if ( + type(contract["contract_version"]) is not int + or contract["contract_version"] != 2 + ): + raise ValueError("Receipt contract_version must be 2.") + seed = _text(contract["seed_column"], "seed_column") + default = _text( + contract["default_assignment_profile"], "default_assignment_profile" + ) + _text(contract["assignment_knob"], "assignment_knob") + profiles = contract["assignment_profiles"] + if not isinstance(profiles, Mapping) or not profiles: + raise ValueError("Receipt assignment_profiles must be a non-empty object.") + for name, profile in profiles.items(): + _text(name, "profile") + if ( + not isinstance(profile, Mapping) + or not {"draw_key_suffix"} <= set(profile) + or set(profile) - {"draw_key_suffix", "exclusion_order"} + ): + raise ValueError("Receipt profile has missing or unknown fields.") + if not isinstance(profile["draw_key_suffix"], str): + raise ValueError("Receipt draw_key_suffix must be text.") + reversed_receipt_groups([], profile.get("exclusion_order", "declared")) + if default not in profiles: + raise ValueError( + "Receipt default_assignment_profile must name an assignment profile." + ) + programs = contract["programs"] + if not isinstance(programs, list | tuple) or not programs: + raise ValueError("Receipt programs must be a non-empty ordered list.") + parsed, identities, draws = [], set(), {} + for row in programs: + required_row = { + "program", + "output", + "judgment_columns", + "judgment_reduction", + "payment_column", + } + allowed = required_row | { + "target", + "strata", + "outside_strata", + "exclude_columns", + *RECEIPT_UNIT_FIELDS, + } + if ( + not isinstance(row, Mapping) + or not required_row <= set(row) + or set(row) - allowed + ): + raise ValueError("Receipt v2 program has missing or unknown fields.") + if ("target" in row) == ("strata" in row): + raise ValueError( + "Receipt v2 program declares exactly one target or strata." + ) + item = dict(row) + for field in ("program", "output", "payment_column"): + item[field] = _text(row[field], field) + item["judgment_columns"] = _strings( + row["judgment_columns"], "judgment_columns", nonempty=True + ) + excludes = row.get("exclude_columns", []) + if not isinstance(excludes, list | tuple) or len(set(excludes)) != len( + excludes + ): + raise ValueError("Receipt exclude_columns must be distinct columns.") + item["exclude_columns"] = [ + _text(column, "exclude column") for column in excludes + ] + if row["judgment_reduction"] not in {"any", "all"}: + raise ValueError("Receipt judgment_reduction must be any or all.") + for field in RECEIPT_UNIT_FIELDS: + item[field] = row.get(field, "person") + if item[field] != "person": + raise ValueError( + f"Receipt non-person {field} reserved for the declared-unit receipt extension." + ) + if "strata" in row: + if not isinstance(row["strata"], list | tuple) or not row["strata"]: + raise ValueError("Receipt strata must be a non-empty list.") + if row.get("outside_strata") != "not_assigned": + raise ValueError( + "Receipt outside_strata must be declared as not_assigned." + ) + for stratum in row["strata"]: + if not isinstance(stratum, Mapping) or set(stratum) != { + "target", + "filter", + }: + raise ValueError("Receipt stratum has missing or unknown fields.") + _text(stratum["target"], "target") + _text(stratum["filter"], "filter") + item["strata"] = [dict(stratum) for stratum in row["strata"]] + else: + if "outside_strata" in row: + raise ValueError("Receipt outside_strata requires strata.") + item["target"] = _text(row["target"], "target") + names = [ + item["program"], + item["output"], + *[s["target"] for s in receipt_strata(item)], + ] + for name in names: + if name in identities: + raise ValueError( + f"Receipt program, output and target identities must be unique: {name!r}." + ) + identities.add(name) + for profile in profiles.values(): + pair = (item["program"], profile["draw_key_suffix"]) + key = receipt_draw_key(*pair) + if key in draws and draws[key] != pair: + raise ValueError("Receipt draw key collision.") + draws[key] = pair + parsed.append(item) + outputs = {row["output"] for row in parsed} + groups = contract["exclusion_groups"] + if not isinstance(groups, list | tuple): + raise ValueError("Receipt exclusion_groups must be a list.") + for group in groups: + names = _strings(group, "exclusion group", nonempty=True) + if len(names) < 2 or not set(names) <= outputs: + raise ValueError( + "Each receipt exclusion group names distinct declared outputs." + ) + unmodelled = _strings( + contract.get("unmodelled_programs", []), "unmodelled_programs" + ) + if set(unmodelled) & identities: + raise ValueError( + "Receipt unmodelled_programs must not name produced outputs or programs." + ) + return seed, parsed, groups + + +def receipt_strata(row): + return row.get("strata", [{"target": row.get("target"), "filter": None}]) + + +def _mass(weights, mask): + return math.fsum(weights[mask].tolist()) + + +def _ratio(target, mass): + return target / mass if mass else (0 if target == 0 else None) + + +def hall_diagnostics(demands, weights): + """Check every subset, using fsum and a declared relative tolerance. + + A demand is (programme, filter, target, eligibility mask). Split people into + equal-neighbourhood weighted supplies. A network joins source to supplies, + supplies to eligible demands, and demands to sink with target capacities. + The max-flow/min-cut theorem says all targets can be filled iff every subset + of demands has total target at most its neighbouring supply (weighted Hall). + Dividing a flow by each person's weight gives mutually exclusive receipt + probabilities, and multiplying such probabilities gives a flow, proving + equivalence for expected counts. This is diagnostic only; draws and priority + may still refuse even when the fractional allocation is feasible. + """ + # Collapse identical neighbourhoods once. Each subsequent cut reads only + # these weighted supplies, rather than scanning the person table again. + if demands: + packed = np.packbits( + np.column_stack([item[3] for item in demands]), axis=1, bitorder="little" + ) + grouped_weights = {} + for packed_pattern, weight in zip(packed, weights, strict=True): + pattern = int.from_bytes(packed_pattern.tobytes(), "little") + if pattern: + grouped_weights.setdefault(pattern, []).append(float(weight)) + supplies = [ + (pattern, math.fsum(grouped_weights[pattern])) + for pattern in sorted(grouped_weights) + ] + else: + supplies = [] + violations = [] + for size in range(1, len(demands) + 1): + for indices in itertools.combinations(range(len(demands)), size): + subset = [demands[index] for index in indices] + subset_pattern = sum(1 << index for index in indices) + total = math.fsum(item[2] for item in subset) + mass = math.fsum( + supply for pattern, supply in supplies if pattern & subset_pattern + ) + if total > mass and not math.isclose( + total, mass, rel_tol=RECEIPT_RELATIVE_TOLERANCE, abs_tol=0 + ): + filters = {item[1] for item in subset} + violations.append( + { + "programs": [item[0] for item in subset], + "filter": next(iter(filters)) if len(filters) == 1 else None, + "filters": [item[1] for item in subset], + "target": total, + "eligible_mass": mass, + } + ) + return { + "violations": violations, + "feasible": not violations, + "relative_tolerance": RECEIPT_RELATIVE_TOLERANCE, + } + + +def assign_receipts_v2( + person, + contract, + *, + weights=None, + targets=None, + bindings=None, + knobs=None, + tables=None, +): + seed, rows, declared_groups = validate_receipt_v2(contract) + if not isinstance(knobs, Mapping) or contract["assignment_knob"] not in knobs: + raise ValueError( + "Receipt assignment_knob must be present in the resolved knobs." + ) + profile_name = knobs[contract["assignment_knob"]] + if profile_name not in contract["assignment_profiles"]: + raise ValueError(f"Receipt unknown assignment profile {profile_name!r}.") + profile = contract["assignment_profiles"][profile_name] + order = profile.get("exclusion_order", "declared") + groups = reversed_receipt_groups(declared_groups, order) + if weights is None or targets is None: + raise ValueError("Receipt v2 targets need a surface and weights.") + weights = np.asarray(weights, dtype=np.float64) + if ( + weights.shape != (len(person),) + or not np.isfinite(weights).all() + or (weights < 0).any() + ): + raise ValueError("Receipt weights must be aligned, finite and non-negative.") + tables = ( + dict(tables) + if tables is not None + else {row[field]: person for row in rows for field in RECEIPT_UNIT_FIELDS} + ) + eligible, masks, target_values = {}, {}, {} + before_payment = {} + for row in rows: + dispatch = receipt_dispatch_tables(tables, row) + judgments = [ + resolve_judgments( + dispatch["entitlement_entity"][name].to_numpy(), + UndeterminedAction.REFUSE, + ) + for name in row["judgment_columns"] + ] + reduction = ( + np.logical_or if row["judgment_reduction"] == "any" else np.logical_and + ) + base = reduction.reduce(judgments) + payment = dispatch["payee"][row["payment_column"]].to_numpy(dtype=np.float64) + if not np.isfinite(payment).all(): + raise ValueError("Receipt payment values must be finite.") + base &= payment > 0 + before_payment[row["output"]] = base.copy() + for column in row["exclude_columns"]: + values = dispatch["entitlement_entity"][column] + if str(values.dtype) not in {"bool", "boolean"} or values.isna().any(): + raise ValueError( + "Existing receipt exclusions must be non-null booleans." + ) + base &= ~values.to_numpy(dtype=bool) + eligible[row["output"]] = base + previous = [] + for stratum in receipt_strata(row): + name, filter_name = stratum["target"], stratum["filter"] + if name not in targets: + raise ValueError( + f"Receipt target {name!r} needs a surface and weights." + ) + value = targets[name] + if ( + isinstance(value, bool) + or not isinstance(value, int | float) + or not math.isfinite(value) + or value < 0 + ): + raise ValueError("Receipt target must be finite and non-negative.") + target_values[name] = value + if filter_name is None: + mask = np.ones(len(person), dtype=bool) + else: + if bindings is None: + raise ValueError("Receipt strata need calibration_bindings.") + mask = calibration_predicate_mask( + tables, bindings, entity=row["projection"], name=filter_name + ) + for previous_name, previous_mask in previous: + if np.any(mask & previous_mask): + raise ValueError( + f"Receipt overlapping strata {previous_name!r} and {filter_name!r} for {row['program']!r}." + ) + previous.append((filter_name, mask)) + masks[name] = mask + audit = { + "assignment_profile": profile_name, + "exclusion_order": order, + "status": "completed", + "programs": {}, + "exclusion_groups": [], + } + for group in groups: + group_rows = [ + row for output in group for row in rows if row["output"] == output + ] + overlaps = [] + filters = {s["filter"] for row in group_rows for s in receipt_strata(row)} - { + None + } + for left, right in itertools.permutations(group_rows, 2): + both = eligible[left["output"]] & eligible[right["output"]] + for filter_name in [None, *sorted(filters)]: + mask = both.copy() + if filter_name is not None: + mask &= calibration_predicate_mask( + tables, + bindings, + entity=left["projection"], + name=filter_name, + ) + overlaps.append( + { + "programs": [left["output"], right["output"]], + "filter": filter_name, + "n_eligible": int(mask.sum()), + "eligible_mass": _mass(weights, mask), + } + ) + demands = [ + ( + row["output"], + s["filter"], + target_values[s["target"]], + eligible[row["output"]] & masks[s["target"]], + ) + for row in group_rows + for s in receipt_strata(row) + ] + # This includes each stratum's 2**k - 1 programme subsets and the + # cross-filter cuts required when a group also has unstratified demands. + feasibility = hall_diagnostics(demands, weights) + audit["exclusion_groups"].append( + { + "order": group, + "status": "completed", + "overlaps": overlaps, + "feasibility": feasibility, + } + ) + by_output = {row["output"]: row for row in rows} + pending = list(by_output) + ordered = [] + while pending: + available = [ + output + for output in pending + if all( + not any(prior in pending for prior in group[: group.index(output)]) + for group in groups + if output in group + ) + ] + if not available: + raise ValueError("Receipt exclusion groups declare a cyclic order.") + output = available[0] + ordered.append(by_output[output]) + pending.remove(output) + id_column = f"{rows[0]['projection']}_id" + out = person.loc[:, [id_column]].copy() + for row in ordered: + output = row["output"] + remaining = eligible[output].copy() + for group in groups: + if output in group: + for prior in group[: group.index(output)]: + remaining &= ~out[prior].to_numpy(dtype=bool) + draws = derive_take_up_draws( + receipt_dispatch_tables(tables, row)["draw_unit"][seed].to_numpy(), + receipt_draw_key(row["program"], profile["draw_key_suffix"]), + ) + assigned, covered = ( + np.zeros(len(person), dtype=bool), + np.logical_or.reduce( + [masks[stratum["target"]] for stratum in receipt_strata(row)] + ), + ) + stratum_audits = [] + total_target = math.fsum( + target_values[stratum["target"]] for stratum in receipt_strata(row) + ) + total_before = _mass(weights, before_payment[output] & covered) + total_remaining = _mass(weights, remaining & covered) + audit["programs"][output] = { + "program": row["program"], + "target": total_target, + "eligible_mass_before_exclusions": total_before, + "eligible_mass": total_remaining, + "target_to_eligible_mass_before_exclusions": _ratio( + total_target, total_before + ), + "target_to_remaining_eligible_mass": _ratio(total_target, total_remaining), + "strata": stratum_audits, + "outside_strata": { + "n_eligible": int((remaining & ~covered).sum()), + "eligible_mass": _mass(weights, remaining & ~covered), + }, + } + for stratum in receipt_strata(row): + name, filter_name = stratum["target"], stratum["filter"] + mask, target = masks[name], target_values[name] + pool = remaining & mask + mass = _mass(weights, pool) + before = _mass(weights, before_payment[output] & mask) + rate = _ratio(target, mass) + detail = { + "target_name": name, + "target": target, + "filter": filter_name, + "eligible_mass": mass, + "eligible_mass_before_exclusions": before, + "target_to_eligible_mass_before_exclusions": _ratio(target, before), + "target_to_remaining_eligible_mass": rate, + "rate": rate, + } + stratum_audits.append(detail) + if target > mass: + audit["status"] = "refused" + for group_audit in audit["exclusion_groups"]: + group_audit["status"] = ( + "completed" + if all(name in out for name in group_audit["order"]) + else "refused" + ) + raise ReceiptAssignmentRefused( + f"Receipt target {name!r} exceeds eligible weighted mass in {order} order.", + audit, + ) + chosen = pool & (draws < rate) + assigned |= chosen + expected = math.fsum([rate * mass]) + detail.update( + { + "realized_mass": _mass(weights, chosen), + "n_receipts": int(chosen.sum()), + "numerical_check": { + "passed": math.isclose( + expected, + target, + rel_tol=RECEIPT_RELATIVE_TOLERANCE, + abs_tol=0, + ), + "relative_tolerance": RECEIPT_RELATIVE_TOLERANCE, + "expected_mass": expected, + }, + } + ) + audit["programs"][output]["realized_mass"] = _mass(weights, assigned) + audit["programs"][output]["n_receipts"] = int(assigned.sum()) + out[output] = assigned + return out.loc[:, [id_column, *by_output]], audit diff --git a/packages/microcosm-build/src/microcosm/build/transport/reference_selection.py b/packages/microcosm-build/src/microcosm/build/transport/reference_selection.py new file mode 100644 index 0000000000..f833434414 --- /dev/null +++ b/packages/microcosm-build/src/microcosm/build/transport/reference_selection.py @@ -0,0 +1,118 @@ +"""Explicit parked-reference exemptions, validated before any row is filtered. + +The resource grammar is ``{schema_version: 1, resources: {resource: {exempt: +[{name, activation_status, reason}]}}}``. Resource names accept the usual JSON +filename or stem. A compiler reads a document without selecting all its rows; +comparators select their declared rows, and receipt targets, quantile-band +references, mass rows and annualisation keys explicitly select named rows. +Composition checks that selection closure before passing a per-resource +``{exempt: [...]}`` document to a compiler. +""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence + + +def _text(value: object, label: str) -> str: + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"{label} must be nonempty text.") + return value + + +def exemption_rows(selection: Mapping) -> tuple[dict, ...]: + """Validate the closed per-resource declaration independently of its rows.""" + if not isinstance(selection, Mapping) or set(selection) != {"exempt"}: + raise ValueError("A reference selection declares exactly an exempt list.") + rows = selection["exempt"] + if isinstance(rows, str | bytes) or not isinstance(rows, Sequence): + raise ValueError("Reference selection exempt must be a list.") + result = [] + seen = set() + for row in rows: + if not isinstance(row, Mapping) or set(row) != { + "name", + "activation_status", + "reason", + }: + raise ValueError( + "A reference exemption declares name, activation_status and reason." + ) + entry = { + key: _text(row[key], f"Reference exemption {key}") + for key in ("name", "activation_status", "reason") + } + if entry["name"] in seen: + raise ValueError(f"Repeated reference exemption {entry['name']!r}.") + seen.add(entry["name"]) + result.append(entry) + return tuple(result) + + +def selection_resources(document: Mapping) -> dict[str, dict]: + """Return declarations keyed by canonical resource stem.""" + if ( + not isinstance(document, Mapping) + or set(document) != {"schema_version", "resources"} + or document["schema_version"] != 1 + or not isinstance(document["resources"], Mapping) + ): + raise ValueError( + "Reference selection requires schema_version 1 and a resources object." + ) + result = {} + for name, selection in document["resources"].items(): + resource = _text(name, "Reference selection resource").removesuffix(".json") + if not resource or resource in result: + raise ValueError(f"Repeated reference selection resource {resource!r}.") + result[resource] = {"exempt": list(exemption_rows(selection))} + return result + + +def validate_exemptions( + document: Mapping, + selection: Mapping, + *, + resource: str = "references", + selected: Mapping[str, Sequence[str]] | None = None, +) -> tuple[dict, ...]: + """Validate against the full effective document, before removing any row. + + ``selected`` maps reference names to the instantiated nodes that select + them. Checking it first ensures a stale or otherwise invalid declaration + can never hide the more fundamental selected-and-exempt contradiction. + """ + exemptions = exemption_rows(selection) + for row in exemptions: + nodes = (selected or {}).get(row["name"], ()) + if nodes: + raise ValueError( + f"Node {nodes[0]!r} selects exempt reference {resource}.{row['name']}." + ) + raw = document.get("target_references") + if isinstance(raw, str | bytes) or not isinstance(raw, Sequence): + raise ValueError(f"{resource}.target_references must be a list.") + references = {} + for row in raw: + if not isinstance(row, Mapping): + raise ValueError(f"{resource} reference must be an object.") + name = row.get("name") + if name in references: + raise ValueError(f"Repeated reference name {name!r} in {resource}.") + references[name] = row + for row in exemptions: + name = row["name"] + if name not in references: + raise ValueError(f"Unknown exempt reference {resource}.{name}.") + metadata = references[name].get("metadata", {}) + if not isinstance(metadata, Mapping): + raise ValueError(f"{resource}.{name} metadata must be an object.") + status = metadata.get("activation_status", "active") + if status in {"", "active"}: + raise ValueError(f"Exempt reference {resource}.{name} is active.") + if status != row["activation_status"]: + raise ValueError( + f"Stale exemption for {resource}.{name}: declared status " + f"{row['activation_status']!r}, effective status {status!r}." + ) + return exemptions diff --git a/packages/microcosm-build/src/microcosm/build/transport/takeup_kernels.py b/packages/microcosm-build/src/microcosm/build/transport/takeup_kernels.py index 69451c7dc1..b1d110a293 100644 --- a/packages/microcosm-build/src/microcosm/build/transport/takeup_kernels.py +++ b/packages/microcosm-build/src/microcosm/build/transport/takeup_kernels.py @@ -610,6 +610,7 @@ class TakeupGapKernel(KernelBase): "facts_sha256", "chronicle_feed_pin", "reference_resolution", + "reference_selection", "annualisation_sha256", "source_household_column", "cell_support", @@ -660,6 +661,24 @@ def run(self, context: KernelContext) -> KernelResult: sha256_param(context, self.ref, "annualisation_sha256") references = parse_reference_document(document, country=country) factors = canonical_document_param(context, self.ref, "annualisation_factors") + reference_selection = ( + canonical_document_param(context, self.ref, "reference_selection") + if "reference_selection" in context.params + else None + ) + if reference_selection is not None: + target_kernels.validate_exemptions( + document, + reference_selection, + selected={name: (context.node.id,) for name in factors}, + ) + exempt_names = { + row["name"] + for row in target_kernels.exemption_rows(reference_selection) + } + references = tuple( + ref for ref in references if ref.name not in exempt_names + ) names = {reference.name for reference in references} if set(factors) != names: raise ValueError( @@ -697,6 +716,7 @@ def run(self, context: KernelContext) -> KernelResult: if "reference_resolution" in context.params else None ), + reference_selection=reference_selection, ) surface = decode_target_surface(surface_payload) problem = target_kernels._compiled_problem( diff --git a/packages/microcosm-build/src/microcosm/build/transport/target_kernels.py b/packages/microcosm-build/src/microcosm/build/transport/target_kernels.py index 46f25de2fa..2b95a01d76 100644 --- a/packages/microcosm-build/src/microcosm/build/transport/target_kernels.py +++ b/packages/microcosm-build/src/microcosm/build/transport/target_kernels.py @@ -49,6 +49,7 @@ import numpy as np import microcosm.build.calibration_columns as calibration_columns_module +import microcosm.build.calibration_predicates as calibration_predicates_module import microcosm.build.chronicle_epoch as chronicle_epoch_module import microcosm.build.country_spec as country_spec_module import microcosm.build.ledger_artifact as ledger_artifact_module @@ -97,6 +98,7 @@ from microcosm.graph.canonical import canonical_json from . import artifact_types, graph_inputs +from . import reference_selection as reference_selection_module from .artifact_types import ( CALIBRATION_PREPARATION_TYPE, COMPARISON_TYPE, @@ -113,6 +115,7 @@ single_source, string_param, ) +from .reference_selection import exemption_rows, validate_exemptions __all__ = [ "TARGET_COMPILATION_KERNELS", @@ -205,6 +208,7 @@ class TargetSurface: facts: Mapping[str, object] sha256: str reference_resolution: Mapping[str, object] | None = None + reference_selection: Mapping[str, object] | None = None def _trace_row(spec: TargetSpec, reference: LedgerTargetReference) -> dict[str, object]: @@ -225,6 +229,7 @@ def encode_target_surface( references_sha256: str, facts: Mapping[str, object], reference_resolution: Mapping[str, object] | None = None, + reference_selection: Mapping[str, object] | None = None, ) -> bytes: """Encode a compiled registry with its one-to-one reference trace.""" @@ -255,6 +260,8 @@ def encode_target_surface( } if reference_resolution is not None: document["reference_resolution"] = dict(reference_resolution) + if reference_selection is not None: + document["reference_selection"] = dict(reference_selection) payload = canonical_json(document) decode_target_surface(payload) return payload @@ -302,7 +309,15 @@ def _surface_resolution(document: Mapping, registry: TargetRegistry) -> Mapping "effective_intervals_sha256" ) != resolution.get("effective_intervals_sha256"): raise ValueError("Surface reference profile identity mismatch.") - if sorted(spec.name for spec in resolved) != resolution.get("transformed_names"): + transformed_names = resolution.get("transformed_names") + if "reference_selection" in document: + exempt_names = { + row["name"] for row in exemption_rows(document["reference_selection"]) + } + transformed_names = [ + name for name in transformed_names if name not in exempt_names + ] + if sorted(spec.name for spec in resolved) != transformed_names: raise ValueError("Surface resolution transformed names mismatch.") return resolution @@ -318,7 +333,7 @@ def decode_target_surface(payload: bytes) -> TargetSurface: if ( document.get("schema_version") != 1 or document.get("kind") != _SURFACE_KIND - or set(document) - {"reference_resolution"} + or set(document) - {"reference_resolution", "reference_selection"} != { "schema_version", "kind", @@ -346,6 +361,13 @@ def decode_target_surface(payload: bytes) -> TargetSurface: references = [row.get("reference") for row in trace] if len(set(references)) != len(references): raise ValueError("Target surface traces two targets to one reference.") + selection = document.get("reference_selection") + if selection is not None: + exemptions = exemption_rows(selection) + if any(row["activation_status"] in _EXECUTABLE_STATUSES for row in exemptions): + raise ValueError("A target surface cannot exempt an active reference.") + if set(references) & {row["name"] for row in exemptions}: + raise ValueError("An exempt reference entered the compiled target surface.") for spec, row in zip(registry.specs, trace, strict=True): if ( row.get("target") != spec.name @@ -365,6 +387,7 @@ def decode_target_surface(payload: bytes) -> TargetSurface: facts=document["facts"], sha256=sha256_text(payload), reference_resolution=_surface_resolution(document, registry), + reference_selection=selection, ) @@ -377,6 +400,7 @@ def compile_target_surface( expected_facts_sha256: str | None = None, feed_pin: Mapping[str, object] | None = None, reference_resolution: Mapping[str, object] | None = None, + reference_selection: Mapping[str, object] | None = None, ) -> tuple[bytes, TargetRegistry]: """Compile a reference document against one consumer artifact. @@ -399,6 +423,10 @@ def compile_target_surface( reference_resolution = validate_reference_resolution( document, reference_resolution, references_sha256=references_sha256 ) + if reference_selection is not None: + exemptions = validate_exemptions(document, reference_selection) + exempt_names = {row["name"] for row in exemptions} + references = tuple(ref for ref in references if ref.name not in exempt_names) artifact = load_ledger_consumer_artifact( facts_path, expected_facts_sha256=expected_facts_sha256, @@ -444,6 +472,7 @@ def compile_target_surface( "n_facts": len(artifact.facts), }, reference_resolution=reference_resolution, + reference_selection=reference_selection, ) return payload, registry @@ -455,6 +484,20 @@ def _registry_from_context( country = string_param(context, ref, "country") document = canonical_document_param(context, ref, "references") + selection = ( + canonical_document_param(context, ref, "reference_selection") + if "reference_selection" in context.params + else None + ) + if selection is not None and ref == "takeup.compare@1": + validate_exemptions( + document, + selection, + selected={ + row["name"]: (context.node.id,) + for row in document.get("target_references", ()) + }, + ) references_sha256 = sha256_param(context, ref, "references_sha256") expected = ( sha256_param(context, ref, "facts_sha256") @@ -478,6 +521,7 @@ def _registry_from_context( if "reference_resolution" in context.params else None ), + reference_selection=selection, ) return registry, payload @@ -527,6 +571,7 @@ def _optional_float(value: float) -> float | None: registry_module, target_module, hierarchy_module, + reference_selection_module, ) _MATRIX_MODULES = ( matrix_module, @@ -558,6 +603,7 @@ def implementation_hash(self) -> str: graph_inputs, artifact_types, calibration_columns_module, + calibration_predicates_module, reference_intervals_module, reference_factors_module, reference_profiles_module, @@ -858,7 +904,12 @@ class TargetsCompileKernel(KernelBase): ) _required = frozenset({"country", "references", "references_sha256"}) _optional = frozenset( - {"facts_sha256", "chronicle_feed_pin", "reference_resolution"} + { + "facts_sha256", + "chronicle_feed_pin", + "reference_resolution", + "reference_selection", + } ) def implementation_hash(self) -> str: @@ -999,7 +1050,12 @@ class TakeupCompareKernel(KernelBase): {"country", "references", "references_sha256", "entities", "weight_entity"} ) _optional = frozenset( - {"facts_sha256", "chronicle_feed_pin", "reference_resolution"} + { + "facts_sha256", + "chronicle_feed_pin", + "reference_resolution", + "reference_selection", + } ) def implementation_hash(self) -> str: diff --git a/packages/microcosm-build/tests/engine_free/shared/test_calibration_columns.py b/packages/microcosm-build/tests/engine_free/shared/test_calibration_columns.py index 2c7c73cd21..3be7afd6bc 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_calibration_columns.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_calibration_columns.py @@ -3,15 +3,17 @@ from __future__ import annotations import copy +import hashlib import json import numpy as np import pandas as pd import pytest -from hypothesis import given, settings +from hypothesis import HealthCheck, given, settings from hypothesis import strategies as st from microcosm.build.calibration_columns import prepare_calibration_columns +from microcosm.build.calibration_predicates import calibration_predicate_mask from microcosm.build.country_spec import _validate_target_references from microcosm.build.reference_profiles import resolve_reference_profiles from microcosm.calibrate import Target, TargetSet @@ -534,3 +536,203 @@ def test_signed_integer_domain_admits_losses_and_refuses_booleans_and_floats(): tables["person"]["net_cents"] = values with pytest.raises(ValueError): prepare_calibration_columns(tables, bindings) + + +def _shared_predicate_bindings(): + return { + "schema_version": 1, + "required_inputs": {"person": ["age", "category"]}, + "input_domains": { + "person": { + "age": {"kind": "nonnegative_integer"}, + "category": {"values": ["a", "b", "c"]}, + } + }, + "columns": [ + { + "entity": "person", + "name": "young_a", + "predicates": [ + {"column": "age", "operator": "ge", "value": 18}, + {"column": "age", "operator": "lt", "value": 25}, + {"column": "category", "operator": "eq", "value": "a"}, + ], + }, + { + "entity": "person", + "name": "older_ab", + "predicates": [ + {"column": "age", "operator": "ge", "value": 25}, + {"column": "category", "operator": "in", "value": ["a", "b"]}, + ], + }, + ], + } + + +def _shared_predicate_tables(): + return { + "person": pd.DataFrame( + {"age": [17, 18, 24, 25, 64], "category": ["a", "a", "b", "b", "c"]} + ) + } + + +def test_receipt_predicate_uses_declared_filter_and_ignores_stale_indicators(): + tables, bindings = _shared_predicate_tables(), _shared_predicate_bindings() + tables["person"]["young_a"] = True + young = calibration_predicate_mask( + tables, bindings, entity="person", name="young_a" + ) + older = calibration_predicate_mask( + tables, bindings, entity="person", name="older_ab" + ) + assert young.tolist() == [False, True, False, False, False] + assert older.tolist() == [False, False, False, True, False] + assert tables["person"]["young_a"].all() + + +@pytest.mark.parametrize( + ("column", "value", "message"), + [ + ("age", None, "contains missing values"), + ("age", -1, "outside its declared domain"), + ("age", 18.5, "outside its declared domain"), + ("age", True, "outside its declared domain"), + ("age", "18", "outside its declared domain"), + ("category", None, "contains missing values"), + ("category", "unknown", "outside its declared domain"), + ], +) +def test_receipt_predicate_validates_inputs_before_mask(column, value, message): + tables, bindings = _shared_predicate_tables(), _shared_predicate_bindings() + tables["person"][column] = tables["person"][column].astype(object) + tables["person"].loc[0, column] = value + with pytest.raises(ValueError, match=message): + calibration_predicate_mask(tables, bindings, entity="person", name="young_a") + + +def test_receipt_predicate_validates_even_required_inputs_absent_from_its_mask(): + tables, bindings = _shared_predicate_tables(), _shared_predicate_bindings() + bindings["columns"][0]["predicates"] = [ + {"column": "age", "operator": "ge", "value": 18} + ] + tables["person"].loc[0, "category"] = "unknown" + with pytest.raises(ValueError, match="Calibration input person.category"): + calibration_predicate_mask(tables, bindings, entity="person", name="young_a") + + +@pytest.mark.parametrize( + ("change", "message"), + [ + ("schema", "require schema_version 1"), + ("required", "need required_inputs and input_domains"), + ("domains", "need required_inputs and input_domains"), + ("columns", "need nonempty columns"), + ("empty_required", "required_inputs.*invalid"), + ("missing_domain", "needs a domain"), + ("unsupported_domain", "unsupported domain"), + ("missing_column", "preparation needs person.age"), + ("missing_table", "needs the 'person' table"), + ], +) +def test_receipt_predicate_validates_complete_binding_inputs(change, message): + tables, bindings = _shared_predicate_tables(), _shared_predicate_bindings() + if change == "schema": + bindings["schema_version"] = 2 + elif change in {"required", "domains"}: + bindings.pop("required_inputs" if change == "required" else "input_domains") + elif change == "columns": + bindings["columns"] = [] + elif change == "empty_required": + bindings["required_inputs"]["person"] = [] + elif change == "missing_domain": + bindings["input_domains"]["person"].pop("age") + elif change == "unsupported_domain": + bindings["input_domains"]["person"]["age"] = {"kind": "unsupported"} + elif change == "missing_column": + tables["person"] = tables["person"].drop(columns="age") + elif change == "missing_table": + tables = {} + with pytest.raises(ValueError, match=message): + calibration_predicate_mask(tables, bindings, entity="person", name="young_a") + + +@pytest.mark.parametrize( + ("change", "message"), + [ + ("missing", "exactly one declared predicate column"), + ("repeated", "exactly one declared predicate column"), + ("constant", "must use predicates"), + ("interval", "must use predicates"), + ("empty", "needs predicates"), + ("not_object", "predicates must be objects"), + ("unvalidated", "needs a validated person input"), + ("missing_value", "needs a validated person input"), + ("operator", "Unsupported calibration predicate"), + ("empty_in", "Unsupported calibration predicate"), + ], +) +def test_receipt_predicate_refuses_undeclared_or_invalid_filter(change, message): + tables, bindings = _shared_predicate_tables(), _shared_predicate_bindings() + entry = bindings["columns"][0] + if change == "missing": + bindings["columns"].pop(0) + elif change == "repeated": + bindings["columns"].append(copy.deepcopy(entry)) + elif change in {"constant", "interval"}: + entry[change] = 1 + elif change == "empty": + entry["predicates"] = [] + elif change == "not_object": + entry["predicates"] = [None] + elif change == "unvalidated": + tables["person"]["undeclared"] = 18 + entry["predicates"][0]["column"] = "undeclared" + elif change == "missing_value": + entry["predicates"][0].pop("value") + elif change == "operator": + entry["predicates"][0]["operator"] = "unsupported" + elif change == "empty_in": + entry["predicates"][0].update(operator="in", value=[]) + with pytest.raises(ValueError, match=message): + calibration_predicate_mask(tables, bindings, entity="person", name="young_a") + + +@settings(max_examples=25, deadline=None, suppress_health_check=[HealthCheck.too_slow]) +@given( + st.lists( + st.tuples( + st.integers(min_value=0, max_value=120), st.sampled_from(["a", "b", "c"]) + ), + min_size=1, + max_size=30, + ) +) +def test_receipt_predicate_masks_match_preparation_for_validated_raw_inputs(rows): + tables = {"person": pd.DataFrame(rows, columns=["age", "category"])} + bindings = _shared_predicate_bindings() + prepared = prepare_calibration_columns(tables, bindings)["person"] + for name in ("young_a", "older_ab"): + np.testing.assert_array_equal( + calibration_predicate_mask(tables, bindings, entity="person", name=name), + prepared[name].to_numpy(), + ) + + +def test_prepare_matches_pinned_legacy_output_digest(): + # Captured from the pre-refactor implementation at 4f333f2c2ee70b6b758586f402d966df7297a2f0. + tables = prepare_calibration_columns(_tables(), _bindings()) + document = { + entity: { + "columns": list(table), + "dtypes": [str(dtype) for dtype in table.dtypes], + "index": table.index.tolist(), + "values": table.to_dict(orient="list"), + } + for entity, table in tables.items() + } + payload = json.dumps(document, sort_keys=True, separators=(",", ":")).encode() + assert hashlib.sha256(payload).hexdigest() == ( + "dca0e085cb3074f75bffc1d964284760a250924ab64464ca26b4b207b17e45ad" + ) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_transport_column_kernels.py b/packages/microcosm-build/tests/engine_free/shared/test_transport_column_kernels.py index 71bb59d08d..52ce03b610 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_transport_column_kernels.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_transport_column_kernels.py @@ -893,6 +893,23 @@ def test_target_driven_receipts_bind_surface_and_report_expected_and_realized_ma artifacts={"surface": replace(artifact, payload=wrong_unit_surface)}, ) ) + # A v1 contract takes none of v2's optional parameters: each refuses as + # unknown, exactly as before v2 existed. + for name in ( + "calibration_bindings", + "calibration_bindings_sha256", + "profile_selection", + ): + with pytest.raises(ValueError, match=rf"unknown parameter\(s\): \['{name}'\]"): + TAKEUP_ASSIGN.run( + _context( + TAKEUP_ASSIGN, + frame, + {**params, name: "{}"}, + (Owned("person", "receives_alpha", "bool"),), + artifacts={"surface": artifact}, + ) + ) def test_wrappers_refuse_missing_resource_ids_missing_columns_and_non_input_overrides( diff --git a/packages/microcosm-build/tests/engine_free/shared/test_transport_receipt_v2.py b/packages/microcosm-build/tests/engine_free/shared/test_transport_receipt_v2.py new file mode 100644 index 0000000000..f3786058e7 --- /dev/null +++ b/packages/microcosm-build/tests/engine_free/shared/test_transport_receipt_v2.py @@ -0,0 +1,944 @@ +"""Shared receipt contracts on synthetic people, never country declarations. + +The v1 oracle below freezes the assignment operation at 4f333f2c; the toy +contract is loaded from the existing composed-graph fixture. V2 deliberately +requires disjoint masks on the actual people rather than overlap precedence. +""" + +from __future__ import annotations + +import copy +import json +import math + +import numpy as np +import pandas as pd +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from microcosm.build.transport import receipt_contract as receipt_module +from microcosm.build.transport.column_kernels import ( + assign_receipts, + validate_receipt_contract, +) +from microcosm.build.transport.receipt_contract import ( + ReceiptAssignmentRefused, + receipt_draw_key, + reversed_receipt_groups, +) +from microcosm.frame.concepts import derive_take_up_draws +from microcosm.frame.input_closure import resolve_judgments +from test_support.microcosm_build.transport_composed import make_composed_fixture + +PROPERTY = settings( + max_examples=18, + deadline=None, + database=None, + suppress_health_check=[HealthCheck.too_slow], +) + + +def _program(name="alpha", *, stratified=False): + row = { + "program": name, + "output": f"receives_{name}", + "judgment_columns": ["eligible"], + "judgment_reduction": "any", + "payment_column": "payment", + } + if stratified: + row.update( + strata=[ + {"target": f"{name}_young", "filter": "young"}, + {"target": f"{name}_older", "filter": "older"}, + ], + outside_strata="not_assigned", + ) + else: + row["target"] = f"{name}_total" + return row + + +def _contract(*programs, groups=None): + return { + "contract_version": 2, + "seed_column": "take_up_seed", + "default_assignment_profile": "central", + "assignment_knob": "assignment", + "assignment_profiles": { + "central": {"draw_key_suffix": ""}, + "variant": {"draw_key_suffix": "variant"}, + "reversed": {"draw_key_suffix": "", "exclusion_order": "reversed"}, + }, + "programs": list(programs) or [_program()], + "exclusion_groups": [] if groups is None else groups, + "unmodelled_programs": ["unsupported"], + } + + +def _people(ages=(19, 23, 31, 45, 61, 72)): + count = len(ages) + return pd.DataFrame( + { + "person_id": np.arange(count, dtype=np.int64) + 1, + "age": list(ages), + "take_up_seed": np.linspace(0.01, 0.99, count), + "eligible": np.ones(count, dtype=np.int64), + "alternate": np.ones(count, dtype=np.int64), + "payment": np.ones(count), + } + ) + + +def _bindings(): + return { + "schema_version": 1, + "required_inputs": {"person": ["age"]}, + "input_domains": {"person": {"age": {"kind": "nonnegative_integer"}}}, + "columns": [ + { + "entity": "person", + "name": "young", + "predicates": [ + {"column": "age", "operator": "ge", "value": 18}, + {"column": "age", "operator": "lt", "value": 30}, + ], + }, + { + "entity": "person", + "name": "older", + "predicates": [ + {"column": "age", "operator": "ge", "value": 30}, + {"column": "age", "operator": "lt", "value": 65}, + ], + }, + ], + } + + +def _assign( + person, + contract=None, + *, + weights=None, + targets=None, + bindings=None, + profile="central", +): + return assign_receipts( + person, + _contract() if contract is None else contract, + weights=np.ones(len(person)) if weights is None else np.asarray(weights), + targets={"alpha_total": 0} if targets is None else targets, + bindings=_bindings() if bindings is None else bindings, + knobs={"assignment": profile}, + ) + + +@pytest.mark.parametrize("location", ["contract", "program", "stratum", "profile"]) +def test_v2_closed_key_sets_refuse_unknown_fields(location): + contract = _contract(_program(stratified=True)) + destination = { + "contract": contract, + "program": contract["programs"][0], + "stratum": contract["programs"][0]["strata"][0], + "profile": contract["assignment_profiles"]["central"], + }[location] + destination["unknown_field"] = "typo" + with pytest.raises(ValueError, match="unknown|fields|declares"): + validate_receipt_contract(contract) + + +@pytest.mark.parametrize("version", [1, 3, True, "2", None]) +def test_v2_refuses_unknown_or_noninteger_contract_version(version): + contract = _contract() + contract["contract_version"] = version + with pytest.raises(ValueError, match="contract_version|version"): + validate_receipt_contract(contract) + + +@pytest.mark.parametrize( + "columns", [[], ["eligible", "eligible"], [""], [None], "eligible"] +) +def test_v2_judgment_columns_are_nonempty_unique_names(columns): + contract = _contract() + contract["programs"][0]["judgment_columns"] = columns + with pytest.raises(ValueError, match="judgment|non-empty|distinct|unique"): + validate_receipt_contract(contract) + + +@pytest.mark.parametrize("reduction", ["some", "majority", "ANY", None]) +def test_v2_refuses_unsupported_judgment_reduction(reduction): + contract = _contract() + contract["programs"][0]["judgment_reduction"] = reduction + with pytest.raises(ValueError, match="reduction"): + validate_receipt_contract(contract) + + +@pytest.mark.parametrize(("reduction", "determined"), [("any", 1), ("all", -1)]) +def test_undetermined_judgment_refuses_before_reduction(reduction, determined): + person = _people((20,)) + person["eligible"] = determined + person["alternate"] = 0 + program = _program() + program.update( + judgment_columns=["eligible", "alternate"], judgment_reduction=reduction + ) + with pytest.raises(ValueError, match="[Uu]ndetermined"): + _assign(person, _contract(program)) + + +@pytest.mark.parametrize( + ("reduction", "expected"), [("any", [True, True]), ("all", [False, True])] +) +def test_v2_reduces_only_determined_judgments(reduction, expected): + person = _people((20, 21)) + person["eligible"] = [1, 1] + person["alternate"] = [-1, 1] + program = _program() + program.update( + judgment_columns=["eligible", "alternate"], judgment_reduction=reduction + ) + out, _ = _assign(person, _contract(program), targets={"alpha_total": sum(expected)}) + assert out["receives_alpha"].tolist() == expected + + +@pytest.mark.parametrize("field", ["program", "output", "target"]) +def test_v2_contract_identities_are_unique(field): + first, second = _program("alpha"), _program("beta") + second[field] = first[field] + with pytest.raises(ValueError, match="distinct|unique|repeat"): + validate_receipt_contract(_contract(first, second)) + + +def test_v2_targets_are_unique_across_strata_and_programs(): + first, second = _program("alpha", stratified=True), _program("beta") + second["target"] = first["strata"][0]["target"] + with pytest.raises( + ValueError, match="target.*distinct|target.*unique|target.*repeat" + ): + validate_receipt_contract(_contract(first, second)) + + +@pytest.mark.parametrize("defect", ["both", "neither"]) +def test_v2_program_declares_target_xor_strata(defect): + program = _program(stratified=True) + if defect == "both": + program["target"] = "total" + else: + del program["strata"] + with pytest.raises(ValueError, match="target.*strata|strata.*target"): + validate_receipt_contract(_contract(program)) + + +@pytest.mark.parametrize("outside", [None, "assign", "false"]) +def test_v2_outside_strata_policy_is_explicit(outside): + program = _program(stratified=True) + if outside is None: + del program["outside_strata"] + else: + program["outside_strata"] = outside + with pytest.raises(ValueError, match="outside_strata|not_assigned"): + validate_receipt_contract(_contract(program)) + + +@pytest.mark.parametrize( + "field", ["entitlement_entity", "payee", "draw_unit", "projection"] +) +def test_reserved_person_defaults_refuse_household(field): + contract = _contract() + _, rows, _ = validate_receipt_contract(contract) + assert rows[0][field] == "person" + contract["programs"][0][field] = "household" + with pytest.raises( + ValueError, match="non-person.*reserved for the declared-unit receipt extension" + ): + validate_receipt_contract(contract) + + +@pytest.mark.parametrize("profile", ["missing", None]) +def test_v2_resolves_only_declared_assignment_profiles(profile): + with pytest.raises(ValueError, match="profile|assignment"): + _assign(_people(), profile=profile) + + +def test_v2_assignment_knob_must_be_present(): + with pytest.raises(ValueError, match="knob|assignment"): + assign_receipts( + _people(), + _contract(), + weights=np.ones(6), + targets={"alpha_total": 0}, + knobs={}, + ) + + +@pytest.mark.parametrize("order", ["random", None, True]) +def test_v2_exclusion_order_is_closed(order): + contract = _contract() + contract["assignment_profiles"]["central"]["exclusion_order"] = order + with pytest.raises(ValueError, match="exclusion_order|declared.*reversed"): + validate_receipt_contract(contract) + + +def test_overlapping_strata_refuse_and_name_both_masks(): + bindings = _bindings() + bindings["columns"][1]["predicates"] = [ + {"column": "age", "operator": "ge", "value": 18} + ] + with pytest.raises( + ValueError, match="[Oo]verlap.*young.*older|young.*older.*overlap" + ): + _assign( + _people(), + _contract(_program(stratified=True)), + targets={"alpha_young": 0, "alpha_older": 0}, + bindings=bindings, + ) + + +@pytest.mark.parametrize("invalid_age", [None, -1, 20.5, "unknown"]) +def test_receipt_predicate_inputs_are_validated_before_masking(invalid_age): + person = _people((20,)) + person["age"] = [invalid_age] + with pytest.raises( + ValueError, + match="Calibration input person.age.*missing|Calibration input person.age.*domain", + ): + _assign( + person, + _contract(_program(stratified=True)), + targets={"alpha_young": 0, "alpha_older": 0}, + ) + + +def test_receipt_predicates_require_declared_validated_inputs(): + bindings = _bindings() + bindings["columns"][0]["predicates"][0]["column"] = "payment" + with pytest.raises(ValueError, match="validated person input"): + _assign( + _people(), + _contract(_program(stratified=True)), + targets={"alpha_young": 0, "alpha_older": 0}, + bindings=bindings, + ) + + +def test_stratum_mask_uses_its_declared_predicate(): + person = _people((20, 40)) + out, audit = _assign( + person, + _contract(_program(stratified=True)), + weights=[2, 3], + targets={"alpha_young": 2, "alpha_older": 0}, + ) + assert out["receives_alpha"].tolist() == [True, False] + assert [ + row["eligible_mass"] for row in audit["programs"]["receives_alpha"]["strata"] + ] == [2, 3] + + +def test_each_stratum_has_its_own_rate_and_realized_mass(): + person = _people((20, 21, 40, 41)) + weights = np.array([2, 2, 1, 1]) + out, audit = _assign( + person, + _contract(_program(stratified=True)), + weights=weights, + targets={"alpha_young": 1, "alpha_older": 1}, + ) + rows = audit["programs"]["receives_alpha"]["strata"] + assert [row["filter"] for row in rows] == ["young", "older"] + assert [row["rate"] for row in rows] == [0.25, 0.5] + draws = derive_take_up_draws( + person["take_up_seed"].to_numpy(), receipt_draw_key("alpha", "") + ) + expected = draws < np.array([0.25, 0.25, 0.5, 0.5]) + assert out["receives_alpha"].tolist() == expected.tolist() + for row, mask in zip( + rows, + [np.array([True, True, False, False]), np.array([False, False, True, True])], + strict=True, + ): + assert row["realized_mass"] == math.fsum(weights[mask & expected].tolist()) + assert row["numerical_check"]["passed"] is True + assert row["numerical_check"]["relative_tolerance"] > 0 + + +def test_rate_uses_mass_remaining_after_earlier_exclusion(): + person = _people((20, 21)) + person["alternate"] = [1, -1] + first, second = _program("alpha"), _program("beta") + first["judgment_columns"] = ["alternate"] + contract = _contract(first, second, groups=[["receives_alpha", "receives_beta"]]) + out, audit = _assign(person, contract, targets={"alpha_total": 1, "beta_total": 1}) + assert out["receives_alpha"].tolist() == [True, False] + assert out["receives_beta"].tolist() == [False, True] + row = audit["programs"]["receives_beta"]["strata"][0] + assert row["eligible_mass"] == row["rate"] == 1 + assert row["target_to_eligible_mass_before_exclusions"] == 0.5 + assert row["target_to_remaining_eligible_mass"] == 1 + program = audit["programs"]["receives_beta"] + assert program["target_to_eligible_mass_before_exclusions"] == 0.5 + assert program["target_to_remaining_eligible_mass"] == 1 + + +def test_v2_existing_exclusion_columns_must_be_distinct(): + program = _program() + program["exclude_columns"] = ["already_receives", "already_receives"] + with pytest.raises( + ValueError, match=r"^Receipt exclude_columns must be distinct columns\.$" + ): + validate_receipt_contract(_contract(program)) + + +@pytest.mark.parametrize( + "values", + [pd.Series([1, 0]), pd.Series([True, None], dtype="boolean")], + ids=["non_boolean", "null"], +) +def test_v2_existing_exclusion_columns_must_be_nonnull_booleans(values): + person = _people((20, 21)) + person["already_receives"] = values + program = _program() + program["exclude_columns"] = ["already_receives"] + with pytest.raises( + ValueError, match=r"^Existing receipt exclusions must be non-null booleans\.$" + ): + _assign(person, _contract(program)) + + +@pytest.mark.parametrize("dtype", ["bool", "boolean"]) +def test_rate_uses_mass_remaining_after_existing_exclusion(dtype): + person = _people((20, 21, 40)) + person["already_receives"] = pd.Series([True, False, False], dtype=dtype) + person["another_receipt"] = pd.Series([False, True, False], dtype=dtype) + program = _program() + program["exclude_columns"] = ["already_receives", "another_receipt"] + out, audit = _assign( + person, _contract(program), weights=[2, 3, 5], targets={"alpha_total": 5} + ) + assert out["receives_alpha"].tolist() == [False, False, True] + detail = audit["programs"]["receives_alpha"]["strata"][0] + assert detail["eligible_mass_before_exclusions"] == 10 + assert detail["eligible_mass"] == 5 + assert detail["rate"] == 1 + assert detail["target_to_eligible_mass_before_exclusions"] == 0.5 + assert detail["realized_mass"] == 5 + assert person["already_receives"].tolist() == [True, False, False] + assert person["another_receipt"].tolist() == [False, True, False] + + +def test_programme_ratios_sum_strata_and_exclude_the_outside_remainder(): + _, audit = _assign( + _people((20, 40, 70)), + _contract(_program(stratified=True)), + weights=[2, 3, 5], + targets={"alpha_young": 1, "alpha_older": 1}, + ) + program = audit["programs"]["receives_alpha"] + assert ( + program["target"] == math.fsum(row["target"] for row in program["strata"]) == 2 + ) + assert ( + program["eligible_mass"] + == math.fsum(row["eligible_mass"] for row in program["strata"]) + == 5 + ) + assert program["target_to_eligible_mass_before_exclusions"] == 0.4 + assert program["target_to_remaining_eligible_mass"] == 0.4 + assert program["outside_strata"]["eligible_mass"] == 5 + assert program["realized_mass"] == math.fsum( + row["realized_mass"] for row in program["strata"] + ) + + +def test_positive_payment_screen_precedes_target_mass(): + person = _people((20, 21)) + person["payment"] = [0, 1] + with pytest.raises(ValueError, match="exceeds eligible weighted mass"): + _assign(person, targets={"alpha_total": 2}) + + +def test_target_above_stratum_mass_refuses_even_when_total_fits(): + with pytest.raises(ValueError, match="alpha_young.*exceeds eligible weighted mass"): + _assign( + _people((20, 40)), + _contract(_program(stratified=True)), + targets={"alpha_young": 2, "alpha_older": 0}, + ) + + +def test_outside_strata_remains_unassigned_and_is_disclosed(): + out, audit = _assign( + _people((10, 20, 70)), + _contract(_program(stratified=True)), + weights=[2, 3, 5], + targets={"alpha_young": 3, "alpha_older": 0}, + ) + assert out["receives_alpha"].tolist() == [False, True, False] + assert audit["programs"]["receives_alpha"]["outside_strata"] == { + "n_eligible": 2, + "eligible_mass": 7, + } + + +def test_overlap_audit_records_overall_and_stratum_counts_before_assignment(): + first, second = ( + _program("alpha", stratified=True), + _program("beta", stratified=True), + ) + contract = _contract(first, second, groups=[["receives_alpha", "receives_beta"]]) + _, audit = _assign( + _people((20, 40, 70)), + contract, + weights=[2, 3, 5], + targets={"alpha_young": 0, "alpha_older": 0, "beta_young": 0, "beta_older": 0}, + ) + group = audit["exclusion_groups"][0] + overlaps = { + row["filter"]: row + for row in group["overlaps"] + if row["programs"] == ["receives_alpha", "receives_beta"] + } + assert (overlaps[None]["n_eligible"], overlaps[None]["eligible_mass"]) == (3, 10) + assert (overlaps["young"]["n_eligible"], overlaps["young"]["eligible_mass"]) == ( + 1, + 2, + ) + assert (overlaps["older"]["n_eligible"], overlaps["older"]["eligible_mass"]) == ( + 1, + 3, + ) + assert group["status"] == audit["status"] == "completed" + + +def test_hall_diagnostic_checks_pairs_when_each_singleton_fits(): + contract = _contract( + _program("alpha"), + _program("beta"), + groups=[["receives_alpha", "receives_beta"]], + ) + with pytest.raises(ReceiptAssignmentRefused) as refused: + _assign( + _people((20, 21)), contract, targets={"alpha_total": 2, "beta_total": 1} + ) + violations = refused.value.audit["exclusion_groups"][0]["feasibility"]["violations"] + assert len(violations) == 1 + assert violations[0]["programs"] == ["receives_alpha", "receives_beta"] + assert violations[0]["target"] == 3 + assert violations[0]["eligible_mass"] == 2 + + +def test_hall_violation_is_a_diagnostic_and_does_not_gate_assignment(): + contract = _contract( + _program("alpha"), + _program("beta"), + groups=[["receives_alpha", "receives_beta"]], + ) + candidates = np.linspace(0, np.nextafter(1.0, 0), 1000) + alpha = derive_take_up_draws(candidates, receipt_draw_key("alpha", "")) + beta = derive_take_up_draws(candidates, receipt_draw_key("beta", "")) + person = _people((20, 21)) + person["take_up_seed"] = candidates[(alpha >= 0.75) & (beta >= 0.75)][:2] + out, audit = _assign( + person, contract, targets={"alpha_total": 1.5, "beta_total": 1.5} + ) + assert not out[["receives_alpha", "receives_beta"]].to_numpy().any() + assert audit["status"] == "completed" + group = audit["exclusion_groups"][0] + assert group["status"] == "completed" + assert group["feasibility"]["feasible"] is False + (violation,) = group["feasibility"]["violations"] + assert violation["programs"] == ["receives_alpha", "receives_beta"] + assert violation["target"] == 3 + assert violation["eligible_mass"] == 2 + + +def test_reversed_profile_refusal_is_raised_with_its_audit_result(): + contract = _contract( + _program("alpha"), + _program("beta"), + groups=[["receives_alpha", "receives_beta"]], + ) + with pytest.raises( + ReceiptAssignmentRefused, match="exceeds eligible weighted mass" + ) as refused: + _assign( + _people((20, 21)), + contract, + targets={"alpha_total": 1, "beta_total": 2}, + profile="reversed", + ) + audit = refused.value.audit + assert audit["assignment_profile"] == "reversed" + assert audit["exclusion_order"] == "reversed" + assert audit["status"] == "refused" + assert audit["exclusion_groups"][0]["order"] == ["receives_beta", "receives_alpha"] + assert audit["exclusion_groups"][0]["status"] == "refused" + + +@pytest.mark.parametrize("first_refuses", [True, False]) +def test_refusal_never_marks_an_unfinished_group_completed(first_refuses): + names = ["alpha", "beta", "gamma", "delta"] + contract = _contract( + *[_program(name) for name in names], + groups=[ + ["receives_alpha", "receives_beta"], + ["receives_gamma", "receives_delta"], + ], + ) + targets = {f"{name}_total": 0 for name in names} + failed_pair = names[:2] if first_refuses else names[2:] + targets[f"{failed_pair[0]}_total"] = 2 + targets[f"{failed_pair[1]}_total"] = 1 + with pytest.raises(ReceiptAssignmentRefused) as refused: + _assign(_people((20, 21)), contract, targets=targets) + statuses = [group["status"] for group in refused.value.audit["exclusion_groups"]] + assert statuses == ( + ["refused", "refused"] if first_refuses else ["completed", "refused"] + ) + + +def test_reversed_profile_reverses_assignment_priority_only(): + contract = _contract( + _program("alpha"), + _program("beta"), + groups=[["receives_alpha", "receives_beta"]], + ) + candidates = np.linspace(0, np.nextafter(1.0, 0), 1000) + alpha = derive_take_up_draws(candidates, receipt_draw_key("alpha", "")) + beta = derive_take_up_draws(candidates, receipt_draw_key("beta", "")) + person = _people((20, 21)) + person["take_up_seed"] = [ + candidates[(alpha < 0.5) & (beta < 0.5)][0], + candidates[(alpha >= 0.5) & (beta >= 0.5)][0], + ] + central, _ = _assign(person, contract, targets={"alpha_total": 1, "beta_total": 1}) + reversed_out, audit = _assign( + person, + contract, + targets={"alpha_total": 1, "beta_total": 1}, + profile="reversed", + ) + assert ( + central["receives_alpha"].tolist() + == reversed_out["receives_beta"].tolist() + == [True, False] + ) + assert ( + central["receives_beta"].tolist() + == reversed_out["receives_alpha"].tolist() + == [False, True] + ) + assert audit["exclusion_groups"][0]["order"] == ["receives_beta", "receives_alpha"] + + +def test_profile_suffix_changes_the_declared_draw_stream(): + person = _people(tuple(range(20, 40))) + out, audit = _assign(person, targets={"alpha_total": 10}, profile="variant") + expected = ( + derive_take_up_draws( + person["take_up_seed"].to_numpy(), receipt_draw_key("alpha", "variant") + ) + < 0.5 + ) + assert out["receives_alpha"].tolist() == expected.tolist() + assert audit["assignment_profile"] == "variant" + + +def test_draw_keys_distinguish_adversarial_program_suffix_pairs(): + pairs = [ + ("a", "bc"), + ("ab", "c"), + ("a:b", "c"), + ("a", "b:c"), + ("a\0b", "c"), + ("a", "b\0c"), + ('["a",', '"b"]'), + ("", "a"), + ] + assert len({receipt_draw_key(*pair) for pair in pairs}) == len(pairs) + + +def test_contract_refuses_a_draw_key_collision(monkeypatch): + monkeypatch.setattr( + receipt_module, "receipt_draw_key", lambda program, suffix: "collision" + ) + with pytest.raises(ValueError, match="[Dd]raw.*collision|collision.*draw"): + validate_receipt_contract(_contract()) + + +@PROPERTY +@given( + st.lists( + st.tuples(st.text(max_size=12), st.text(max_size=12)), + min_size=1, + max_size=15, + unique=True, + ) +) +def test_property_draw_key_is_injective(pairs): + assert len({receipt_draw_key(*pair) for pair in pairs}) == len(pairs) + + +@PROPERTY +@given( + st.lists( + st.lists(st.text(min_size=1, max_size=5), min_size=2, max_size=5, unique=True), + min_size=0, + max_size=3, + ) +) +def test_property_reversing_exclusion_order_twice_is_declared(groups): + reversed_once = reversed_receipt_groups(groups, "reversed") + assert reversed_receipt_groups( + reversed_once, "reversed" + ) == reversed_receipt_groups(groups, "declared") + + +@PROPERTY +@given( + st.lists(st.integers(min_value=1, max_value=1000), min_size=2, max_size=12), + st.floats(min_value=0, max_value=1, allow_nan=False), +) +def test_property_per_stratum_numerical_check(weights, fraction): + ages = [20 if index % 2 else 40 for index in range(len(weights))] + person = _people(tuple(ages)) + masses = { + "alpha_young": math.fsum( + weight for age, weight in zip(ages, weights, strict=True) if age < 30 + ), + "alpha_older": math.fsum( + weight for age, weight in zip(ages, weights, strict=True) if age >= 30 + ), + } + _, audit = _assign( + person, + _contract(_program(stratified=True)), + weights=weights, + targets={name: mass * fraction for name, mass in masses.items()}, + ) + for row in audit["programs"]["receives_alpha"]["strata"]: + check = row["numerical_check"] + assert check["passed"] is True + assert math.isclose( + math.fsum([row["rate"] * row["eligible_mass"]]), + row["target"], + rel_tol=check["relative_tolerance"], + abs_tol=0, + ) + + +@PROPERTY +@given( + st.lists(st.integers(min_value=0, max_value=90), min_size=1, max_size=15), st.data() +) +def test_property_disjoint_strata_and_outside_partition_eligible_mass(ages, data): + weights = data.draw( + st.lists(st.integers(0, 100), min_size=len(ages), max_size=len(ages)) + ) + eligible = data.draw( + st.lists(st.booleans(), min_size=len(ages), max_size=len(ages)) + ) + person = _people(tuple(ages)) + person["eligible"] = np.where(eligible, 1, -1) + _, audit = _assign( + person, + _contract(_program(stratified=True)), + weights=weights, + targets={"alpha_young": 0, "alpha_older": 0}, + ) + program = audit["programs"]["receives_alpha"] + partition = math.fsum( + [ + *(row["eligible_mass"] for row in program["strata"]), + program["outside_strata"]["eligible_mass"], + ] + ) + assert partition == math.fsum( + weight for weight, permitted in zip(weights, eligible, strict=True) if permitted + ) + assert program["eligible_mass"] == math.fsum( + row["eligible_mass"] for row in program["strata"] + ) + + +@PROPERTY +@given(st.integers(1, 15), st.floats(min_value=0, max_value=0.25, allow_nan=False)) +def test_property_exclusion_group_never_assigns_two_flags(count, fraction): + contract = _contract( + _program("alpha"), + _program("beta"), + groups=[["receives_alpha", "receives_beta"]], + ) + try: + out, _ = _assign( + _people(tuple(range(20, 20 + count))), + contract, + targets={"alpha_total": count * fraction, "beta_total": count * fraction}, + ) + except ReceiptAssignmentRefused as refused: + assert refused.audit["status"] == "refused" + else: + assert (out[["receives_alpha", "receives_beta"]].sum(axis=1) <= 1).all() + + +def _v1_before_change(person, contract, *, weights=None, targets=None): + """Freeze the valid-input v1 assignment loop from HEAD 4f333f2c, independently. + + This intentionally does not call the evolving contract validator or receipt + implementation; generated contracts below are all valid v1 declarations. + """ + out = person.loc[:, ["person_id"]].copy() + audit = {} + for row in contract["programs"]: + output = row["output"] + eligible = resolve_judgments( + person[row["judgment_column"]].to_numpy(), "refuse" + ) + if row.get("payment_column") is not None: + eligible &= person[row["payment_column"]].to_numpy(dtype=np.float64) > 0 + for column in row.get("exclude_columns", []): + eligible &= ~person[column].to_numpy(dtype=bool) + for group in contract["exclusion_groups"]: + if output in group: + for previous in group: + if previous in out: + eligible &= ~out[previous].to_numpy() + target = None + mass = None if weights is None else math.fsum(weights[eligible].tolist()) + if "target" in row: + target = targets[row["target"]] + rate = target / mass if mass else target + else: + rate = row["rate"] + draws = derive_take_up_draws( + person[contract["seed_column"]].to_numpy(), row["program"] + ) + assigned = eligible & (draws < rate) + out[output] = assigned + audit[output] = { + "program": row["program"], + "rate": rate, + "target": target, + "eligible_mass": mass, + "n_receipts": int(assigned.sum()), + "realized_mass": None + if weights is None + else math.fsum(weights[assigned].tolist()), + } + return out, audit + + +def _assert_v1_bytes(person, contract, *, weights=None, targets=None): + before, before_audit = _v1_before_change( + person, contract, weights=weights, targets=targets + ) + after, after_audit = assign_receipts( + person, contract, weights=weights, targets=targets + ) + assert ( + before.to_records(index=False).tobytes() + == after.to_records(index=False).tobytes() + ) + assert ( + json.dumps(before_audit, sort_keys=True, separators=(",", ":")).encode() + == json.dumps(after_audit, sort_keys=True, separators=(",", ":")).encode() + ) + + +def test_v1_toy_flags_and_audit_bytes_match_frozen_before_change(tmp_path): + fixture = make_composed_fixture(tmp_path / "toy") + contract = fixture.spec["resources"]["receipt_contract"]["receipts"] + person = _people() + person["toy_main_eligible"] = [1, -1, 1, 1, -1, 1] + person["toy_pension_eligible"] = [1, 1, 1, -1, 1, 1] + person["toy_main_payment"] = [1, 1, 0, 2, -1, 1] + person["toy_pension_payment"] = [1, 0, 1, 1, 1, 1] + _assert_v1_bytes(person, contract, weights=np.array([1, 2, 3, 4, 5, 6])) + + +@PROPERTY +@given( + st.lists(st.sampled_from([-1, 1]), min_size=1, max_size=15), + st.floats(min_value=0, max_value=1, allow_nan=False), + st.data(), +) +def test_property_v1_flags_and_audit_bytes_match_before_change(judgments, rate, data): + count = len(judgments) + person = _people(tuple(range(20, 20 + count))) + person["eligible"] = judgments + person["existing"] = data.draw( + st.lists(st.booleans(), min_size=count, max_size=count) + ) + person["payment"] = data.draw( + st.lists(st.integers(-2, 2), min_size=count, max_size=count) + ) + weights = np.asarray( + data.draw(st.lists(st.integers(0, 100), min_size=count, max_size=count)), + dtype=np.float64, + ) + contract = { + "seed_column": "take_up_seed", + "programs": [ + { + "program": "toy.alpha", + "output": "receives_alpha", + "judgment_column": "eligible", + "payment_column": "payment", + "exclude_columns": ["existing"], + "rate": rate, + }, + { + "program": "toy.beta", + "output": "receives_beta", + "judgment_column": "eligible", + "rate": rate, + }, + ], + "exclusion_groups": [["receives_alpha", "receives_beta"]], + } + _assert_v1_bytes(person, contract, weights=weights) + target_contract = copy.deepcopy(contract) + target_contract["programs"] = [target_contract["programs"][0]] + target_contract["exclusion_groups"] = [] + del target_contract["programs"][0]["rate"] + target_contract["programs"][0]["target"] = "toy_target" + permitted = ( + (person["eligible"].to_numpy() == 1) + & (person["payment"].to_numpy() > 0) + & ~person["existing"].to_numpy() + ) + target = math.fsum(weights[permitted].tolist()) * rate + _assert_v1_bytes( + person, target_contract, weights=weights, targets={"toy_target": target} + ) + + +def test_hall_diagnostic_checks_mixed_unstratified_and_stratified_subsets(): + people = _people((19, 23, 31)) + people["eligible"] = [1, 1, -1] + alpha, beta = _program(), _program("beta", stratified=True) + beta["judgment_columns"] = ["alternate"] + contract = _contract(alpha, beta, groups=[["receives_alpha", "receives_beta"]]) + # The two young people cannot supply alpha's 2 and beta.young's 1. + # beta's separate older pool makes programme-total and individual-filter + # checks pass, so only a mixed-filter demand subset exposes infeasibility. + with pytest.raises(ReceiptAssignmentRefused) as refused: + _assign( + people, + contract, + weights=[1, 1, 100], + targets={"alpha_total": 2, "beta_young": 1, "beta_older": 0}, + ) + feasibility = refused.value.audit["exclusion_groups"][0]["feasibility"] + assert feasibility["feasible"] is False + assert any( + violation["programs"] == ["receives_alpha", "receives_beta"] + and violation["filters"] == [None, "young"] + and violation["target"] == 3 + and violation["eligible_mass"] == 2 + for violation in feasibility["violations"] + ) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_transport_receipt_v2_kernel.py b/packages/microcosm-build/tests/engine_free/shared/test_transport_receipt_v2_kernel.py new file mode 100644 index 0000000000..c391394f77 --- /dev/null +++ b/packages/microcosm-build/tests/engine_free/shared/test_transport_receipt_v2_kernel.py @@ -0,0 +1,188 @@ +"""Receipt surface validation and the reserved declared-unit dispatch seam.""" + +import copy +import json +from types import SimpleNamespace + +import numpy as np +import pandas as pd +import pytest + +from microcosm.build.transport.artifact_types import TARGET_SURFACE_TYPE +from microcosm.build.transport.column_kernels import TAKEUP_ASSIGN +from microcosm.build.transport.receipt_contract import receipt_dispatch_tables +from microcosm.build.transport.target_kernels import compile_target_surface +from microcosm.graph import ArtifactValue, KernelContext, Node, NumericScope, Owned +from test_support.microcosm_build.transport_graph import ( + canonical_text, + reference_document, + reference_row, + toy_fact, + write_facts, +) +from test_support.microcosm_build.transport_population import digest + + +def _context( + tmp_path, + *, + filter_name="adult", + entity="person", + measure="receives_alpha", + output="receives_alpha", + unit="count", +): + contract = { + "contract_version": 2, + "seed_column": "seed", + "default_assignment_profile": "central", + "assignment_knob": "assignment", + "assignment_profiles": {"central": {"draw_key_suffix": ""}}, + "programs": [ + { + "program": "alpha", + "output": "receives_alpha", + "judgment_columns": ["eligible"], + "judgment_reduction": "any", + "payment_column": "payment", + "strata": [{"target": "alpha_target", "filter": "adult"}], + "outside_strata": "not_assigned", + } + ], + "exclusion_groups": [], + "unmodelled_programs": ["unsupported"], + } + bindings = { + "schema_version": 1, + "required_inputs": {"person": ["age"], "household": ["size"]}, + "input_domains": { + "person": {"age": {"kind": "nonnegative_integer"}}, + "household": {"size": {"kind": "nonnegative_integer"}}, + }, + "columns": [ + { + "entity": "person", + "name": "adult", + "predicates": [{"column": "age", "operator": "ge", "value": 18}], + } + ], + } + document = reference_document( + [ + reference_row( + "alpha_target", entity=entity, measure=measure, filter_=filter_name + ) + ] + ) + fact = toy_fact("alpha_target", 1, entity=entity) + fact["observed_measure"]["unit"] = unit + facts = write_facts(tmp_path / "facts.jsonl", [fact]) + surface, _ = compile_target_surface( + facts, document, country="xx", references_sha256=digest(document) + ) + params = { + "receipt_contract": canonical_text(contract), + "receipt_contract_sha256": digest(contract), + "calibration_bindings": canonical_text(bindings), + "calibration_bindings_sha256": digest(bindings), + "profile_selection": canonical_text({"assignment": "central"}), + } + node = Node( + "receipt", + TAKEUP_ASSIGN.ref, + population="open", + params=params, + outputs=(Owned("person", output, "bool"),), + ) + return KernelContext( + node=node, + tables={ + "person": pd.DataFrame( + { + "person_id": [1], + "age": [20], + "seed": [0.2], + "eligible": [1], + "payment": [1], + } + ), + "household": pd.DataFrame({"household_id": [1], "size": [1]}), + }, + weights={"person": SimpleNamespace(values=np.ones(1))}, + strata=pd.DataFrame(), + params=node.params, + rng=np.random.default_rng(1), + artifacts={ + "surface": ArtifactValue( + payload=surface, + type=TARGET_SURFACE_TYPE, + key="0" * 64, + producer_key="0" * 64, + numerics=NumericScope(), + ) + }, + ) + + +def test_v2_kernel_checks_declared_stratum_filter(tmp_path): + with pytest.raises(ValueError, match="declared stratum filter"): + TAKEUP_ASSIGN.run(_context(tmp_path, filter_name="different")) + + +@pytest.mark.parametrize( + "field,value", [("entity", "household"), ("measure", "other"), ("unit", "currency")] +) +def test_v2_kernel_requires_declared_count_surface(tmp_path, field, value): + with pytest.raises(ValueError, match="person count"): + TAKEUP_ASSIGN.run(_context(tmp_path, **{field: value})) + + +@pytest.mark.parametrize("output", ["unsupported", "receives_unknown"]) +def test_unmodelled_or_unknown_output_never_defaults_false(tmp_path, output): + with pytest.raises(ValueError, match="unmodelled or unknown receipt outputs"): + TAKEUP_ASSIGN.run(_context(tmp_path, output=output)) + + +def test_v2_kernel_validates_full_bindings_before_preparation(tmp_path): + context = _context(tmp_path) + result = TAKEUP_ASSIGN.run(context) + assert result.columns["person", "receives_alpha"].tolist() == [True] + assert result.receipt["programs"]["receives_alpha"]["strata"][0]["numerical_check"][ + "passed" + ] + context.tables["household"].loc[0, "size"] = -1 + with pytest.raises(ValueError, match="Calibration input household.size"): + TAKEUP_ASSIGN.run(context) + + +@pytest.mark.parametrize( + "field", ["entitlement_entity", "payee", "draw_unit", "projection"] +) +def test_dispatch_reads_each_reserved_field_with_registered_fake_entity(field): + person, fake = object(), object() + row = dict.fromkeys( + ["entitlement_entity", "payee", "draw_unit", "projection"], "person" + ) + row[field] = "registered_fake" + result = receipt_dispatch_tables({"person": person, "registered_fake": fake}, row) + assert result[field] is fake + assert all(value is person for name, value in result.items() if name != field) + + +def test_v2_kernel_resolves_profile_from_declared_knob(tmp_path): + context = _context(tmp_path) + params = copy.deepcopy(dict(context.params)) + params["profile_selection"] = json.dumps( + {"unrelated": "central"}, sort_keys=True, separators=(",", ":") + ) + context = KernelContext( + node=context.node, + tables=context.tables, + weights=context.weights, + strata=context.strata, + params=params, + rng=context.rng, + artifacts=context.artifacts, + ) + with pytest.raises(ValueError, match="assignment_knob"): + TAKEUP_ASSIGN.run(context) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_transport_reference_selection.py b/packages/microcosm-build/tests/engine_free/shared/test_transport_reference_selection.py new file mode 100644 index 0000000000..558c46be7a --- /dev/null +++ b/packages/microcosm-build/tests/engine_free/shared/test_transport_reference_selection.py @@ -0,0 +1,665 @@ +"""Selection closure, parked-row compilation and frozen legacy differentials.""" + +from __future__ import annotations + +import ast +import json +from copy import deepcopy + +import numpy as np +import pandas as pd +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from microcosm.build.transport import compose, takeup_kernels, target_kernels +from microcosm.build.transport.artifact_types import COMPARISON_TYPE +from microcosm.build.transport.reference_selection import ( + exemption_rows, + selection_resources, +) +from microcosm.graph import ( + ArtifactOutput, + Capabilities, + Determinism, + Graph, + KernelContext, + Node, + Owned, + StructuralDelta, + compile_graph, +) +from microcosm.graph.canonical import canonical_json +from microcosm.graph.keys import node_key +from test_support.microcosm_build.transport_graph import ( + reference_document, + reference_row, + toy_fact, + toy_frame, + write_facts, +) +from test_support.paths import paths_for + +SHA = "7" * 64 +CONFIG = compose.TransportGraphConfig({}, (Owned("person", "person_id", "int64"),)) + + +def _document(names=("used", "parked")): + return reference_document( + [ + reference_row( + name, + entity="person", + measure="person_count", + activation_status="pending" if name == "parked" else "active", + ) + for name in names + ] + ) + + +def _exemption(name="parked", status="pending"): + return {"name": name, "activation_status": status, "reason": "Awaiting producer."} + + +def _selection(*rows): + return {"exempt": list(rows or (_exemption(),))} + + +def _node(kernel="targets.compile@1", id="reader", **params): + return { + "id": id, + "kernel": kernel, + "population": "open", + "params": { + "country": "xx", + "references": {"resource": "target_references", "encoding": "json"}, + "references_sha256": { + "resource": "target_references", + "encoding": "sha256", + }, + **params, + }, + } + + +def _spec(*nodes): + return { + "country": "xx", + "resources": { + "transport_graph": {"nodes": list(nodes or (_node(),))}, + "benefit_unit_rule": {"dependent_child": {"max_age": 18}}, + "axiom_rules_bindings": {"engine": {"commit": "a", "wheel_sha256": "b"}}, + "scenarios": {"scenarios": []}, + "precal_references": _document(("precal",)), + "target_references": _document(), + "holdout_references": _document(("holdout",)), + "receipt_contract": { + "receipts": { + "seed_column": "seed", + "programs": [ + { + "program": "benefit", + "output": "receives_benefit", + "judgment_column": "eligible", + "payment_column": "payment", + "rate": 1, + } + ], + "exclusion_groups": [], + } + }, + "reference_selection": { + "schema_version": 1, + "resources": {"target_references": _selection()}, + }, + }, + } + + +def _legacy(namespace, names): + """Execute only frozen pure definitions, with today's shared dependencies. + + Holding the implementation digest constant below isolates composition's + node-key contract. Editing an attested kernel module intentionally changes + its implementation digest, outside the legacy selection-path guarantee. + """ + path = ( + paths_for("microcosm-build").tests + / "fixtures/reference_selection_legacy.py.txt" + ) + tree = ast.parse(path.read_text()) + definitions = [ + ast.get_source_segment(path.read_text(), node) + for node in tree.body + if isinstance(node, ast.FunctionDef) and node.name in names + ] + scope = dict(vars(namespace)) + exec("from __future__ import annotations\n" + "\n\n".join(definitions), scope) + return scope + + +def test_resource_read_does_not_select_parked_rows(): + spec = _spec() + compose.validate_transport_activation(spec) + node = compose._node(spec, _node(), CONFIG, {}) + assert json.loads(node.params["reference_selection"]) == _selection() + assert not compose._reference_selection_closure(spec)["target_references"] + + +def _v2_spec(): + assignment = { + "id": "assign", + "kernel": "takeup.assign@1", + "population": "open", + "params": { + "receipt_contract": { + "resource": "receipt_contract", + "path": ["receipts"], + "encoding": "json", + }, + "receipt_contract_sha256": { + "resource": "receipt_contract", + "encoding": "sha256", + }, + }, + } + spec = _spec(_node(), assignment) + spec["resources"]["receipt_contract"]["receipts"] = { + "contract_version": 2, + "seed_column": "seed", + "default_assignment_profile": "central", + "assignment_knob": "receipt_profile", + "assignment_profiles": { + "central": {"draw_key_suffix": "central"}, + "reverse": {"draw_key_suffix": "reverse", "exclusion_order": "reversed"}, + }, + "programs": [ + { + "program": "benefit", + "output": "receives_benefit", + "judgment_columns": ["eligible"], + "judgment_reduction": "any", + "payment_column": "payment", + "strata": [{"target": "used", "filter": "adult"}], + "outside_strata": "not_assigned", + } + ], + "exclusion_groups": [], + "unmodelled_programs": [], + } + spec["resources"]["scenarios"]["central_knobs"] = {"receipt_profile": "central"} + spec["resources"]["calibration_bindings"] = { + "schema_version": 1, + "required_inputs": {"person": ["age"]}, + "input_domains": {"person": {"age": {"kind": "integer", "minimum": 0}}}, + "columns": [ + { + "entity": "person", + "name": "adult", + "predicates": [{"column": "age", "operator": "ge", "value": 18}], + } + ], + } + return spec, assignment + + +def test_v2_preflight_checks_default_profile_and_stratum_predicate(): + spec, _ = _v2_spec() + compose.validate_transport_activation(spec) + spec["resources"]["scenarios"]["central_knobs"]["receipt_profile"] = "reverse" + with pytest.raises( + ValueError, match="default_assignment_profile must equal central_knobs" + ): + compose.validate_transport_activation(spec) + spec["resources"]["scenarios"]["central_knobs"]["receipt_profile"] = "central" + spec["resources"]["receipt_contract"]["receipts"]["programs"][0]["strata"][0][ + "filter" + ] = "unbound" + with pytest.raises( + ValueError, + match="Receipt stratum 'unbound' must name calibration_bindings predicates", + ): + compose.validate_transport_activation(spec) + + +def test_v2_node_binds_predicates_and_resolves_declared_scenario_knob(): + spec, assignment = _v2_spec() + node = compose._node( + spec, assignment, CONFIG, {}, profile_selection={"receipt_profile": "reverse"} + ) + assert ( + json.loads(node.params["calibration_bindings"]) + == spec["resources"]["calibration_bindings"] + ) + assert node.params["calibration_bindings_sha256"] == compose._resource_digest( + spec, "calibration_bindings" + ) + assert json.loads(node.params["profile_selection"]) == { + "receipt_profile": "reverse" + } + + +def test_v2_unstratified_preflight_needs_no_predicate_resource(): + spec, assignment = _v2_spec() + program = spec["resources"]["receipt_contract"]["receipts"]["programs"][0] + program.pop("strata") + program.pop("outside_strata") + program["target"] = "used" + spec["resources"].pop("calibration_bindings") + compose.validate_transport_activation(spec) + node = compose._node(spec, assignment, CONFIG, {}) + assert "calibration_bindings" not in node.params + + +def test_exempt_selected_passes_mutant_comparison_refuses_before_filtering(): + spec = _spec(_node("takeup.compare@1", "explicit-comparison")) + with pytest.raises( + ValueError, match="explicit-comparison.*exempt.*target_references.parked" + ): + compose.validate_transport_activation(spec) + + +@pytest.mark.parametrize( + ("parameter", "value"), + [ + ("receipt_contract", {"programs": [{"target": "parked"}]}), + ( + "receipt_contract", + {"programs": [{"strata": [{"target": "parked", "filter": "age"}]}]}, + ), + ("bands", {"bands": [{"reference": "parked"}]}), + ("annualisation_factors", {"parked": 52}), + ("target", "parked"), + ("reference", "parked"), + ], + ids=[ + "receipt-target", + "receipt-stratum", + "quantile-band", + "annualisation-factor", + "direct-target", + "direct-reference", + ], +) +def test_required_target_exempted_mutant_names_consumer(parameter, value): + spec = _spec(_node(id="required-consumer", **{parameter: value})) + with pytest.raises(ValueError, match="required-consumer.*exempt.*parked"): + compose.validate_transport_activation(spec) + + +def test_mass_reference_rows_are_required(): + spec = _spec() + spec["resources"]["mass_references"] = _document(("parked",)) + spec["resources"]["reference_selection"]["resources"]["mass_references"] = ( + _selection() + ) + with pytest.raises( + ValueError, match="mass_references.*exempt.*mass_references.parked" + ): + compose.validate_transport_activation(spec) + + +@pytest.mark.parametrize("tier", ["entitlement", "calibration"]) +def test_closure_uses_instantiated_scenario_and_variant_rows(tier): + spec = _spec() + spec["resources"]["scenarios"]["scenarios"] = [ + {"id": "case", "tier": tier, "knobs": {}} + ] + template = "scenario_nodes" if tier == "entitlement" else "variant_nodes" + spec["resources"]["entitlement_graph"] = { + "schema_version": 1, + "nodes": [], + template: [_node("takeup.compare@1", "compare-{scenario}-{variant}")], + "scenario_gap": "gap-{scenario}", + "variant_gap": "gap-{variant}", + } + with pytest.raises(ValueError, match="compare-.*case.*exempt.*parked"): + compose.validate_transport_activation(spec) + # Explicit nodes replace the parked-row template, matching the factory. + spec["resources"]["scenarios"]["scenarios"][0]["nodes"] = [_node(id="reader-case")] + compose.validate_transport_activation(spec) + + +@pytest.mark.parametrize( + ("mutation", "message"), + [ + ("active", "Exempt reference target_references.parked is active"), + ("stale", "Stale exemption for target_references.parked"), + ("unknown", "Unknown exempt reference target_references.unknown"), + ], + ids=["active", "stale", "unknown"], +) +def test_exempt_active_row_and_stale_unknown_exemptions_refuse(mutation, message): + spec = _spec() + if mutation == "active": + spec["resources"]["target_references"]["target_references"][1]["metadata"][ + "activation_status" + ] = "active" + elif mutation == "stale": + spec["resources"]["reference_selection"]["resources"]["target_references"] = ( + _selection(_exemption(status="old")) + ) + else: + spec["resources"]["reference_selection"]["resources"]["target_references"] = ( + _selection(_exemption("unknown")) + ) + with pytest.raises(ValueError, match=message): + compose.validate_transport_activation(spec) + + +def test_nonexempt_placeholder_keeps_legacy_gap_message(): + spec = _spec() + spec["resources"]["reference_selection"]["resources"] = {} + with pytest.raises( + ValueError, match="placeholder reference target_references.parked" + ): + compose.validate_transport_activation(spec) + + +def test_absent_selection_preserves_complete_legacy_activation_gap_order(): + spec = _spec() + spec["resources"].pop("reference_selection") + spec["resources"]["holdout_references"] = _document(("parked",)) + spec["resources"]["precal_references"] = _document(("parked",)) + # The assigned baseline already visits the reference-resource set in + # sorted order, including mandatory resources selected by multiple params. + with pytest.raises(ValueError) as error: + compose.validate_transport_activation(spec) + assert str(error.value) == ( + "Transport spec is not activated: " + "placeholder reference holdout_references.parked; " + "placeholder reference precal_references.parked; " + "placeholder reference target_references.parked." + ) + + +@pytest.mark.parametrize( + ("selection", "message"), + [ + ({"exempt": [], "ignored": True}, "exactly an exempt list"), + ({"exempt": "parked"}, "exempt must be a list"), + ( + {"exempt": [{"name": "parked"}]}, + "declares name, activation_status and reason", + ), + (_selection({**_exemption(), "reason": ""}), "reason must be nonempty text"), + (_selection(_exemption(), _exemption()), "Repeated reference exemption"), + ], +) +def test_exemption_schema_is_closed(selection, message): + with pytest.raises(ValueError, match=message): + exemption_rows(selection) + + +def test_selection_resource_schema_and_filename_aliases(): + assert selection_resources( + {"schema_version": 1, "resources": {"a.json": _selection()}} + ) == {"a": _selection()} + with pytest.raises(ValueError, match="Repeated reference selection resource"): + selection_resources( + { + "schema_version": 1, + "resources": {"a": _selection(), "a.json": _selection()}, + } + ) + with pytest.raises(ValueError, match="schema_version 1 and a resources object"): + selection_resources({"schema_version": 1, "resources": {}, "ignored": True}) + + +def test_compile_validates_full_document_and_records_exemptions(tmp_path): + facts = write_facts( + tmp_path / "facts.jsonl", [toy_fact("used", 3, entity="person")] + ) + payload, registry = target_kernels.compile_target_surface( + facts, + _document(), + country="xx", + references_sha256=SHA, + reference_selection=_selection(), + ) + surface = target_kernels.decode_target_surface(payload) + assert [spec.name for spec in registry.specs] == ["used"] + assert [row["reference"] for row in surface.trace] == ["used"] + assert surface.reference_selection == _selection() + with pytest.raises(ValueError, match="Unknown exempt reference references.unknown"): + target_kernels.compile_target_surface( + facts, + _document(), + country="xx", + references_sha256=SHA, + reference_selection=_selection(_exemption("unknown")), + ) + + +def test_direct_comparison_request_for_exempt_row_refuses(tmp_path): + node = Node( + "direct-comparison", + "takeup.compare@1", + population="open", + sources=("facts",), + params={ + "country": "xx", + "references": canonical_json(_document()).decode(), + "references_sha256": SHA, + "reference_selection": canonical_json(_selection()).decode(), + "entities": ("person",), + "weight_entity": "person", + }, + artifact_outputs=(ArtifactOutput("comparison", COMPARISON_TYPE),), + ) + context = KernelContext( + node=node, + params=node.params, + tables={}, + weights={}, + strata=pd.Series([], dtype=object), + rng=np.random.default_rng(0), + sources={"facts": tmp_path / "not-opened.jsonl"}, + artifacts={}, + ) + with pytest.raises( + ValueError, match="direct-comparison.*exempt reference references.parked" + ): + target_kernels.TAKEUP_COMPARE.run(context) + + +@pytest.mark.parametrize("request_parked", [False, True]) +def test_gap_compares_only_annualisation_selected_rows(tmp_path, request_parked): + document = _document() + document["target_references"][0]["measure"] = "is_adult" + facts = write_facts( + tmp_path / "facts.jsonl", [toy_fact("used", 3, entity="person")] + ) + factors = {"used": 52, **({"parked": 52} if request_parked else {})} + node = Node( + "explicit-gap", + "takeup.gap@1", + population="open", + sources=("facts",), + params={ + "country": "xx", + "references": canonical_json(document).decode(), + "references_sha256": SHA, + "reference_selection": canonical_json(_selection()).decode(), + "entities": ("person", "household", "family"), + "weight_entity": "household", + "annualisation_factors": canonical_json(factors).decode(), + "scenario": "central", + }, + artifact_outputs=(ArtifactOutput("gap", takeup_kernels.GAP_TYPE),), + ) + frame = toy_frame() + context = KernelContext( + node=node, + params=node.params, + tables={entity: frame.table(entity) for entity in node.params["entities"]}, + weights={"household": frame.weights_for("household")}, + strata=frame.strata, + rng=np.random.default_rng(0), + sources={"facts": facts}, + artifacts={}, + ) + if request_parked: + with pytest.raises( + ValueError, match="explicit-gap.*exempt reference references.parked" + ): + takeup_kernels.TAKEUP_GAP.run(context) + else: + result = takeup_kernels.TAKEUP_GAP.run(context) + assert [row["name"] for row in json.loads(result.artifacts["gap"])["gaps"]] == [ + "used" + ] + + +@settings( + max_examples=12, + suppress_health_check=[HealthCheck.function_scoped_fixture, HealthCheck.too_slow], + deadline=None, +) +@given( + st.lists( + st.from_regex(r"parked_[a-z]{1,6}", fullmatch=True), + min_size=1, + max_size=5, + unique=True, + ) +) +def test_exempt_rows_never_enter_surface(tmp_path, names): + rows = _document(("used",))["target_references"] + [ + reference_row( + name, entity="person", measure="person_count", activation_status="pending" + ) + for name in names + ] + selection = _selection(*(_exemption(name) for name in names)) + facts = write_facts( + tmp_path / "facts.jsonl", [toy_fact("used", 3, entity="person")] + ) + payload, registry = target_kernels.compile_target_surface( + facts, + reference_document(rows), + country="xx", + references_sha256=SHA, + reference_selection=selection, + ) + assert {spec.name for spec in registry.specs} == {"used"} + assert { + row["name"] + for row in target_kernels.decode_target_surface(payload).reference_selection[ + "exempt" + ] + } == set(names) + + +@pytest.mark.parametrize("country", ["xx", "be"]) +def test_legacy_selection_path_changed_mutant_frozen_surface_params_and_keys( + tmp_path, country +): + if country == "xx": + document = _document(("used",)) + fact = toy_fact("used", 3, entity="person") + spec = {"country": country, "resources": {"target_references": document}} + else: + # Keep an actual active scalar row from the packaged country verbatim; + # supply only its synthetic matching fact, never a production feed. + spec = compose.load_transport_spec(country) + document = deepcopy(compose.transport_resource(spec, "target_references")) + document = { + key: value + for key, value in document.items() + if key in target_kernels.REFERENCE_DOCUMENT_KEYS + } + reference = next( + row + for row in document["target_references"] + if row["name"] == "spf_finances_pit_total" + ) + document["target_references"] = [reference] + fact = toy_fact(reference["name"], 3, entity=reference["entity"]) + fact["period"] = {"type": "tax_year", "value": reference["period"]} + fact["geography"]["id"] = country.upper() + for section in ("source", "observed_measure"): + fact[section]["source_name"] = reference["ledger_selector"]["source_name"] + fact["observed_measure"]["source_measure_id"] = reference["ledger_selector"][ + "source_measure_id" + ] + spec["resources"]["target_references"] = document + facts = write_facts(tmp_path / "facts.jsonl", [fact]) + legacy_targets = _legacy( + target_kernels, {"compile_target_surface", "encode_target_surface"} + ) + before, _ = legacy_targets["compile_target_surface"]( + facts, document, country=country, references_sha256=SHA + ) + after, _ = target_kernels.compile_target_surface( + facts, document, country=country, references_sha256=SHA + ) + assert before == after + legacy_compose = _legacy(compose, {"_node", "_reference_activation_gaps"}) + row = _node() + row["params"]["country"] = country + before_node = legacy_compose["_node"](spec, row, CONFIG, {}) + after_node = compose._node(spec, row, CONFIG, {}) + assert before_node.params == after_node.params + assert "reference_selection" not in after_node.params + assert list( + legacy_compose["_reference_activation_gaps"](document, "target_references") + ) == list(compose._reference_activation_gaps(document, "target_references")) + keys = [] + for node in (before_node, after_node): + graph = Graph( + country, + (), + ( + Node( + "open", + "fixture.create@1", + structural=StructuralDelta.CREATE, + outputs=CONFIG.create_outputs, + ), + node, + ), + ) + keys.append( + node_key( + compile_graph(graph), + node.id, + {"open": SHA}, + SHA, + {}, + kernel_capabilities=Capabilities(Determinism.DETERMINISTIC), + ) + ) + assert keys[0] == keys[1] + + +def test_full_effective_resolution_hash_survives_exemption_filtering(tmp_path): + from microcosm.build.reference_profiles import ( + reference_document_sha256, + resolve_reference_profiles, + ) + + document = _document() + declaration = { + "schema_version": 1, + "reference_resource": "target_references", + "dimensions": {}, + } + # A resolver receipt is validated against the full document before parked + # rows leave compilation, never against a reconstructed active subset. + effective, _, receipt = resolve_reference_profiles(document, declaration, {}) + facts = write_facts( + tmp_path / "facts.jsonl", [toy_fact("used", 3, entity="person")] + ) + payload, _ = target_kernels.compile_target_surface( + facts, + effective, + country="xx", + references_sha256=reference_document_sha256(effective), + reference_resolution=receipt, + reference_selection=_selection(), + ) + surface = target_kernels.decode_target_surface(payload) + assert surface.references_sha256 == receipt["effective_references_sha256"] + assert surface.reference_resolution == receipt diff --git a/packages/microcosm-build/tests/fixtures/reference_selection_legacy.py.txt b/packages/microcosm-build/tests/fixtures/reference_selection_legacy.py.txt new file mode 100644 index 0000000000..8725ea04a5 --- /dev/null +++ b/packages/microcosm-build/tests/fixtures/reference_selection_legacy.py.txt @@ -0,0 +1,239 @@ +# Frozen differential oracle from 4f333f2c2ee70b6b758586f402d966df7297a2f0. +# Only these pure functions are retained; imports come from the current modules. + +def _reference_activation_gaps(document: Mapping, resource: str) -> Iterator[str]: + for row in _sequence( + document.get("target_references"), f"{resource}.target_references" + ): + reference = _mapping(row, f"{resource} reference") + metadata = _mapping(reference.get("metadata", {}), f"{resource}.metadata") + if metadata.get("activation_status", "active") != "active": + yield f"placeholder reference {resource}.{reference.get('name', '?')}" + + +def _node( + spec: Mapping[str, object], + row: Mapping, + config: TransportGraphConfig, + types: Mapping[tuple[str, str], ArtifactType], + *, + allowed_kernels: frozenset[str] = SKELETON_KERNELS, + resolved_params: Mapping[str, object] | None = None, + profile_selection: Mapping | None = None, +) -> Node: + _safe_node_id(row.get("id")) + unknown = set(row) - _NODE_FIELDS + if unknown: + raise ValueError( + f"Transport node {row.get('id')!r} has unknown fields {sorted(unknown)}." + ) + kernel = row.get("kernel") + if kernel not in allowed_kernels: + raise ValueError( + f"Kernel {kernel!r} is not a skeleton kernel; add it through an extension." + ) + params = ( + dict(resolved_params) + if resolved_params is not None + else { + name: _param(spec, value, config.engine_refs) + for name, value in _mapping(row.get("params", {}), "node params").items() + } + ) + params = _target_feed_params(spec, kernel, params) + if ( + kernel in TARGET_COMPILATION_KERNELS | {"calibration.prepare@1"} + and "references" in params + ): + view = reference_profile_view(spec, profile_selection) + if view is not None: + references, intervals, receipt = view + if canonical_json(references).decode("utf-8") == params["references"]: + params["reference_resolution"] = canonical_json(receipt).decode("utf-8") + # Preparation receives the exact effective interval companion. + if kernel == "calibration.prepare@1" and intervals is not None: + companion = canonical_json(intervals).decode("utf-8") + if params.setdefault("intervals", companion) != companion: + raise ValueError( + f"Node {row.get('id')!r} supplies an interval document " + "that differs from its effective interval companion." + ) + if "rules_binding" in row: + params = _rules_params(spec, row, params, config) + elif kernel == RULES_ROUTER: + raise ValueError("Every skeleton rules node must name its rules_binding.") + structural = StructuralDelta(row.get("structural", "none")) + if structural is StructuralDelta.CREATE: + if row.get("outputs"): + raise ValueError("CREATE's outputs come from preparation, not the spec.") + outputs = config.create_outputs + else: + outputs = tuple( + Owned( + **{ + **value, + "ownership": Ownership(value.get("ownership", "produced")), + } + ) + for value in _sequence(row.get("outputs", ()), "owned outputs") + ) + inputs = tuple( + Slice(**{**value, "columns": tuple(value["columns"])}) + for value in _sequence(row.get("inputs", ()), "input slices") + ) + artifacts = [] + for value in _sequence(row.get("artifact_inputs", ()), "artifact inputs"): + edge = _mapping(value, "artifact input") + if set(edge) - {"name", "producer", "artifact"}: + raise ValueError( + f"Node {row['id']!r}: an artifact input declares name, producer " + "and optionally artifact; its type is the producer's." + ) + producer, artifact = edge["producer"], edge.get("artifact", edge["name"]) + if (producer, artifact) not in types: + raise ValueError( + f"Node {row['id']!r} reads undeclared artifact {producer}.{artifact}." + ) + artifacts.append( + ArtifactInput(edge["name"], producer, artifact, types[producer, artifact]) + ) + weights = row.get("weights") + return Node( + id=row["id"], + kernel=kernel, + inputs=inputs, + outputs=outputs, + params=params, + population=row.get("population"), + structural=structural, + base=row.get("base"), + sources=tuple(_sequence(row.get("sources", ()), "node sources")), + weights=None if weights is None else WeightTransition(**weights), + mass=row.get("mass", "conserve"), + description=row.get("description", ""), + citation=row.get("citation", ""), + artifact_inputs=tuple(artifacts), + artifact_outputs=_artifact_outputs(row), + ) + + +def encode_target_surface( + registry: TargetRegistry, + references: Sequence[LedgerTargetReference], + *, + references_sha256: str, + facts: Mapping[str, object], + reference_resolution: Mapping[str, object] | None = None, +) -> bytes: + """Encode a compiled registry with its one-to-one reference trace.""" + + specs = registry.specs + if len(specs) != len(references) or any( + spec.name != reference.name + for spec, reference in zip(specs, references, strict=True) + ): + raise ValueError( + "A target surface needs exactly one compiled target per reference, " + "in reference order." + ) + document = { + "schema_version": 1, + "kind": _SURFACE_KIND, + "country": registry.country, + "registry": { + "country": registry.country, + "version": registry.version, + "specs": [spec.to_dict() for spec in specs], + }, + "trace": [ + _trace_row(spec, reference) + for spec, reference in zip(specs, references, strict=True) + ], + "references_sha256": references_sha256, + "facts": dict(facts), + } + if reference_resolution is not None: + document["reference_resolution"] = dict(reference_resolution) + payload = canonical_json(document) + decode_target_surface(payload) + return payload + + +def compile_target_surface( + facts_path: Path, + document: Mapping[str, object], + *, + country: str, + references_sha256: str, + expected_facts_sha256: str | None = None, + feed_pin: Mapping[str, object] | None = None, + reference_resolution: Mapping[str, object] | None = None, +) -> tuple[bytes, TargetRegistry]: + """Compile a reference document against one consumer artifact. + + Returns the surface bytes and the registry they encode. The artifact + payload records content identities only, never the source path, so moving + a source file changes neither a node key nor an artifact byte. A declared + country feed supplies ``feed_pin`` for all six artifact identities; the + optional facts hash remains available for feeds without a full pin. + """ + + references = parse_reference_document(document, country=country) + resolved = any( + ref.metadata.get("reference_profiles_resolved") for ref in references + ) + if resolved and reference_resolution is None: + raise ValueError( + "Profile-resolved references require a reference resolution receipt." + ) + if reference_resolution is not None: + reference_resolution = validate_reference_resolution( + document, reference_resolution, references_sha256=references_sha256 + ) + artifact = load_ledger_consumer_artifact( + facts_path, + expected_facts_sha256=expected_facts_sha256, + feed_pin=feed_pin, + lexical_floats=bool( + reference_resolution and reference_resolution.get("selected_factors") + ), + ) + if reference_resolution is not None: + for entry in reference_resolution.get("selected_factors", {}).values(): + factor_feed_identity = ledger_artifact_module.chronicle_feed_identity( + feed_pin + if feed_pin is not None + else entry["declaration"]["feed_identity"] + ) + if feed_pin is None: + ledger_artifact_module.validate_chronicle_feed_pin( + artifact, factor_feed_identity + ) + if (artifact.manifest or {}).get( + "source_commit" + ) != factor_feed_identity["source_commit"]: + raise ValueError( + "Factor source commit cannot be authenticated without a feed pin." + ) + reference_factors_module.replay_reference_factor( + entry["declaration"], + artifact.facts, + factor_feed_identity, + entry["operational_factor"], + entry["receipt"], + ) + registry = compile_ledger_target_references( + artifact.facts, references, country=country + ) + payload = encode_target_surface( + registry, + references, + references_sha256=references_sha256, + facts={ + "facts_sha256": artifact.facts_sha256, + "manifest_sha256": artifact.manifest_sha256, + "n_facts": len(artifact.facts), + }, + reference_resolution=reference_resolution, + ) + return payload, registry From d8425aeb2b1f25369741b2b7f6473df5cd9412aa Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 25 Oct 2026 22:36:53 -0700 Subject: [PATCH 13/24] Fix two reference-selection fixtures that refused before their assertions Three tests in test_transport_reference_selection.py failed at 2f220f70c for fixture reasons, not because of the code they test: - test_direct_comparison_request_for_exempt_row_refuses gave takeup.compare@1 `entities=("person",)`. entities_param (since G5b) needs the person entity and at least one group entity, so the kernel refused on the parameter shape before it reached the exempt-reference check. It now passes ("person", "household") and reaches the refusal it asserts. - test_legacy_selection_path_changed_mutant_frozen_surface_params_and_keys[xx] and [be] built a CREATE node with no sources, which Node refuses ("a CREATE node needs sources"). The fixture graph now declares one source, `seed`, and the CREATE node reads it. The node keys compared are those of the target node, which both sides build the same way. The file was owed by the build lane and not run before; draft CI doesn't run. Co-Authored-By: Claude Opus 5.5 --- .../shared/test_transport_reference_selection.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_transport_reference_selection.py b/packages/microcosm-build/tests/engine_free/shared/test_transport_reference_selection.py index 558c46be7a..b24b34d4df 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_transport_reference_selection.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_transport_reference_selection.py @@ -26,6 +26,7 @@ KernelContext, Node, Owned, + SourceRef, StructuralDelta, compile_graph, ) @@ -443,7 +444,7 @@ def test_direct_comparison_request_for_exempt_row_refuses(tmp_path): "references": canonical_json(_document()).decode(), "references_sha256": SHA, "reference_selection": canonical_json(_selection()).decode(), - "entities": ("person",), + "entities": ("person", "household"), "weight_entity": "person", }, artifact_outputs=(ArtifactOutput("comparison", COMPARISON_TYPE),), @@ -610,11 +611,12 @@ def test_legacy_selection_path_changed_mutant_frozen_surface_params_and_keys( for node in (before_node, after_node): graph = Graph( country, - (), + (SourceRef("seed", "raw-bytes-v1"),), ( Node( "open", "fixture.create@1", + sources=("seed",), structural=StructuralDelta.CREATE, outputs=CONFIG.create_outputs, ), From fca69c1afc8cce86079a2d359fe5deea43fe0be3 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 25 Oct 2026 23:01:03 -0700 Subject: [PATCH 14/24] Qualify the nil-band sentence in the intervals changelog Zero is nil only when the table declares a nil band (_position checks nil_present first). Without one, a zero record with a zero first cut falls below the first cut under right closure and into the first band under left closure, so the unqualified sentence overstated it. Co-Authored-By: Claude Opus 5.5 --- changelog.d/nz-reference-intervals.added.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/changelog.d/nz-reference-intervals.added.md b/changelog.d/nz-reference-intervals.added.md index 95e52379ed..34cd83efed 100644 --- a/changelog.d/nz-reference-intervals.added.md +++ b/changelog.d/nz-reference-intervals.added.md @@ -16,8 +16,9 @@ each variant its own effective interval companion. Declare the NZ national (237 bands) and age (183 bands) taxable-income tables under Microcosm's declared convention; the 474 IRD references stay parked. -Require the first cut explicitly. Zero is nil under either closure, so with a -nil band and a zero first cut the first positive band starts above zero. Report below-first-cut mass and floored negative records, including +Require the first cut explicitly. When a nil band is present, zero is nil under +either closure, so with a zero first cut the first positive band starts above +zero. Report below-first-cut mass and floored negative records, including weighted counts and signed pre-floor amounts by destination. These diagnostics describe fixed weights before fitting; reporting later weight changes on unfitted records remains G8-r2c/G8-x work. From db02ae8520f0fe6748980b225193d5fd89233d62 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 25 Oct 2026 23:05:12 -0700 Subject: [PATCH 15/24] Scope the target-period invariant and the AST test's claim The module docstring said every applied factor ages money to the declared target period. That holds for money.normalize@1, which checks each selected factor through money_factor_inputs; a direct caller of normalize_money_tables supplies its factors unchecked. Say so. The AST test's comment said it checks each cited fact. It checks the cited constant and call expressions, so an in-memory reassignment of the writer's period would still pass. Say what it checks. Co-Authored-By: Claude Opus 5.5 --- .../src/microcosm/build/money_normalization.py | 9 ++++++--- .../engine_free/shared/test_nz_money_normalization.py | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/microcosm-build/src/microcosm/build/money_normalization.py b/packages/microcosm-build/src/microcosm/build/money_normalization.py index 280a38c3f8..d23b2b79fb 100644 --- a/packages/microcosm-build/src/microcosm/build/money_normalization.py +++ b/packages/microcosm-build/src/microcosm/build/money_normalization.py @@ -26,9 +26,12 @@ Every normalized column maps through its concept's index family to one declared factor, or to an explicit identity with a reason; nothing falls back -to a factor of one. Every applied factor ages money to the one declared -``target_period``. Every money concept that survives transport is declared -exactly once: normalized, quantile mapped or pending. +to a factor of one. Every factor ``money.normalize@1`` applies ages money to +the one declared ``target_period``: the kernel checks each selected factor +through ``money_factor_inputs``, while a direct caller of +``normalize_money_tables`` supplies its factors unchecked. Every money concept +that survives transport is declared exactly once: normalized, quantile mapped +or pending. """ from __future__ import annotations diff --git a/packages/microcosm-build/tests/engine_free/shared/test_nz_money_normalization.py b/packages/microcosm-build/tests/engine_free/shared/test_nz_money_normalization.py index a1d9185555..e1fe039bed 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_nz_money_normalization.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_nz_money_normalization.py @@ -151,7 +151,8 @@ def test_donor_money_basis_cites_model_lineage_as_a_declared_convention(): def test_donor_money_basis_evidence_matches_the_code_it_cites(): # The evidence names symbols, not lines (test_nz_spec_package refuses local - # line citations), so each cited fact is checked against the code itself. + # line citations), so the test checks the cited constant and call + # expressions against the code itself. repository = paths_for("microcosm-build").repository tool = ast.parse( (repository / "tools/build_us_fiscal_refresh_release.py").read_text() From 51080462d3b76464f7e4439e9e943652449239fe Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 26 Oct 2026 05:30:09 -0700 Subject: [PATCH 16/24] Fix the receipt v2 review findings and hash what the surface decoder reads The first review of this PR (Opus 5.5 at d8425aeb2, APPROVE_WITH_NITS) found nothing blocking. This commit fixes its code findings. The weighted Hall diagnostic checked every subset of a group's demands, so its cost doubled with each demand. The review measured 5.2 s for 16 demands on 20,000 people and estimated hours for 24, all before any assignment ran. It now computes an exact max-flow over the same network (Edmonds-Karp in rationals). Each demand's capacity is (1 - r) times its target, which is the old rule: a subset violates when its supply is below (1 - r) times its target. When the plan is infeasible, it reports one set, the sink side of a minimum cut, which is the most deficient set, with the same record keys. A Hypothesis test checks feasibility and the reported deficiency against an exact brute force over up to 8 demands, a test pins the tolerance boundary, and a 24-demand case on 20,000 people runs with subset enumeration patched to raise (0.11 s in the lane's run). A failed expected-mass check now refuses with the audit attached, as a target above the remaining mass already did; before, the audit recorded passed: false and the assignment completed. v2 also refuses non-finite weights, and weight sums that overflow, with ValueError. v1 is unchanged, including its existing OverflowError on an overflowing sum. One limit remains, as before: a reported cut whose total is above the float range cannot be written as a float. Tests now cover the two surface-decode guards (an active reference marked exempt, and an exempt reference inside a compiled surface) and the resolution filter for an exempt row transformed by a profile dimension. decode_target_surface reads reference_profiles and reference_selection, but implementation hashes are not transitive, and five kernels that decode the surface did not hash either module: takeup.assign@1, targets.problem@1, the gate battery, diagnostics.calibration and geography support. They now do, and a test checks all ten surface consumers. Their cache keys move for every country. A search found no golden or test that pins them. Activation now checks each calibration input's domain, through the same function preparation uses, when v2 strata are declared, and contract validation refuses cyclic exclusion groups; both refused only at run time before. The test fixture's unsupported "integer" domain is now nonnegative_integer. The v2 takeup.assign node's profile_selection now carries only the assignment knob, so an unrelated knob no longer changes its key. An assignment knob missing from the resolved knobs refuses with ValueError. The draw-key property test now draws adversarial delimiters, and the exclusion property test is derandomized with a guaranteed overlap, so each fails under its mutant on every run. In the v2 pure function, an output named like a structural id column refuses, as the kernel's column check does; supplied entity tables must match the person argument's length and ids, and a missing one refuses; NumPy real targets are accepted and booleans still refuse. The v2 kernel refuses calibration_bindings without its sha256, and the reverse. v1 still matches the base commit on 1,500 mixed and 3,000 valid generated contracts, and calibration.prepare on 300 cases. Belgium OK'd the src hunks (sha256 0a6d4d16) before this commit. Co-Authored-By: GPT-6.1 Sol Co-Authored-By: Claude Opus 5.5 --- .../microcosm/build/calibration_predicates.py | 35 ++- .../build/transport/column_kernels.py | 8 + .../src/microcosm/build/transport/compose.py | 11 +- .../microcosm/build/transport/gate_kernels.py | 2 + .../build/transport/geography_kernels.py | 2 + .../build/transport/receipt_contract.py | 242 +++++++++++++----- .../build/transport/target_kernels.py | 2 + .../build/transport/terminal_kernels.py | 2 + .../shared/test_transport_receipt_v2.py | 182 ++++++++++++- .../test_transport_receipt_v2_kernel.py | 21 ++ .../test_transport_reference_selection.py | 222 +++++++++++++++- 11 files changed, 642 insertions(+), 87 deletions(-) diff --git a/packages/microcosm-build/src/microcosm/build/calibration_predicates.py b/packages/microcosm-build/src/microcosm/build/calibration_predicates.py index 13d3816235..67bf9bf08b 100644 --- a/packages/microcosm-build/src/microcosm/build/calibration_predicates.py +++ b/packages/microcosm-build/src/microcosm/build/calibration_predicates.py @@ -16,12 +16,13 @@ def validate_calibration_inputs( - tables: Mapping[str, pd.DataFrame], bindings: Mapping[str, Any] + tables: Mapping[str, pd.DataFrame] | None, bindings: Mapping[str, Any] ) -> tuple[Mapping[str, Any], list[Any] | tuple[Any, ...]]: """Validate every declared input using preparation's original contract. Return the declarations so callers can validate their outputs without repeating - schema or input validation. No table or column is mutated. + schema or input validation. ``None`` validates the schema without data, for + activation preflight. No table or column is mutated. """ if bindings.get("schema_version") != 1: raise ValueError("Calibration bindings require schema_version 1.") @@ -33,18 +34,21 @@ def validate_calibration_inputs( if not isinstance(columns, (list, tuple)) or not columns: raise ValueError("Calibration bindings need nonempty columns.") for entity, names in required.items(): - if entity not in tables: + if tables is not None and entity not in tables: raise ValueError(f"Calibration preparation needs the {entity!r} table.") if not isinstance(names, (list, tuple)) or not names: raise ValueError(f"Calibration required_inputs for {entity!r} are invalid.") entity_domains = domains.get(entity, {}) for name in names: - if name not in tables[entity]: + if tables is not None and name not in tables[entity]: raise ValueError(f"Calibration preparation needs {entity}.{name}.") domain = entity_domains.get(name) if not isinstance(domain, Mapping): raise ValueError(f"Calibration input {entity}.{name} needs a domain.") - _validate_input(tables[entity][name], domain, f"{entity}.{name}") + if tables is None: + _input_domain_kind(domain, f"{entity}.{name}") + else: + _validate_input(tables[entity][name], domain, f"{entity}.{name}") return required, columns @@ -142,27 +146,36 @@ def _identity(value: object) -> tuple[type, object]: def _validate_input(values: pd.Series, domain: Mapping[str, Any], name: str) -> None: if values.isna().any(): raise ValueError(f"Calibration input {name} contains missing values.") - if domain.get("kind") == "identity": + kind = _input_domain_kind(domain, name) + if kind == "identity": for value in values.tolist(): _identity(value) valid = True - elif domain.get("kind") == "signed_integer": + elif kind == "signed_integer": valid = all( isinstance(value, Integral) and not isinstance(value, (bool, np.bool_)) for value in values.tolist() ) - elif domain.get("kind") == "nonnegative_integer": + elif kind == "nonnegative_integer": valid = all( _finite_number(value) and value >= 0 and value == int(value) for value in values.tolist() ) - elif isinstance(domain.get("values"), (list, tuple)) and domain["values"]: + else: allowed = domain["values"] valid = all( any(type(value) is type(member) and value == member for member in allowed) for value in values.tolist() ) - else: - raise ValueError(f"Calibration input {name} has an unsupported domain.") if not valid: raise ValueError(f"Calibration input {name} is outside its declared domain.") + + +def _input_domain_kind(domain: Mapping[str, Any], name: str) -> str: + """Share domain-schema validation between activation and table validation.""" + kind = domain.get("kind") + if kind in ("identity", "signed_integer", "nonnegative_integer"): + return kind + if isinstance(domain.get("values"), (list, tuple)) and domain["values"]: + return "values" + raise ValueError(f"Calibration input {name} has an unsupported domain.") diff --git a/packages/microcosm-build/src/microcosm/build/transport/column_kernels.py b/packages/microcosm-build/src/microcosm/build/transport/column_kernels.py index bfa7eadf58..fd062678db 100644 --- a/packages/microcosm-build/src/microcosm/build/transport/column_kernels.py +++ b/packages/microcosm-build/src/microcosm/build/transport/column_kernels.py @@ -703,6 +703,8 @@ def implementation_hash(self) -> str: concepts_module, closure_module, target_kernels, + target_kernels.reference_profiles_module, + target_kernels.reference_selection_module, registry_module, hierarchy_module, calibration_predicates_module, @@ -840,6 +842,12 @@ def _run_v2(self, context, contract): f"Receipt target {name!r} must be a {row['projection']} count of {row['output']!r} with declared stratum filter {stratum['filter']!r}." ) targets[name] = target.value + if ("calibration_bindings" in context.params) != ( + "calibration_bindings_sha256" in context.params + ): + raise ValueError( + f"{self.ref} calibration_bindings and its sha256 must be supplied together." + ) bindings = ( _document(context, self.ref, "calibration_bindings") if "calibration_bindings" in context.params diff --git a/packages/microcosm-build/src/microcosm/build/transport/compose.py b/packages/microcosm-build/src/microcosm/build/transport/compose.py index a41847478a..7794214d10 100644 --- a/packages/microcosm-build/src/microcosm/build/transport/compose.py +++ b/packages/microcosm-build/src/microcosm/build/transport/compose.py @@ -66,6 +66,7 @@ from types import MappingProxyType from typing import Protocol +from microcosm.build.calibration_predicates import validate_calibration_inputs from microcosm.build.country_spec import load_country_spec from microcosm.build.graph_atomic_geography import ( ATOMIC_GEOGRAPHY_VALIDATION_TYPE, @@ -1139,6 +1140,8 @@ def validate_transport_activation(spec: Mapping[str, object]) -> None: if strata_declared else {} ) + if strata_declared: + validate_calibration_inputs(None, bindings) filters = { (entry.get("entity"), entry.get("name")) for entry in bindings.get("columns", ()) @@ -1741,8 +1744,14 @@ def _node( spec, "calibration_bindings" ) central = transport_resource(spec, "scenarios").get("central_knobs", {}) + assignment_knob = receipt_contract["assignment_knob"] + selected_knobs = {**central, **(profile_selection or {})} + if assignment_knob not in selected_knobs: + raise ValueError( + f"Receipt assignment_knob {assignment_knob!r} is not a resolved knob." + ) params["profile_selection"] = canonical_json( - {**central, **(profile_selection or {})} + {assignment_knob: selected_knobs[assignment_knob]} ).decode("utf-8") if ( kernel in TARGET_COMPILATION_KERNELS | {"calibration.prepare@1"} diff --git a/packages/microcosm-build/src/microcosm/build/transport/gate_kernels.py b/packages/microcosm-build/src/microcosm/build/transport/gate_kernels.py index 9ae6a4ec4e..a133a0f211 100644 --- a/packages/microcosm-build/src/microcosm/build/transport/gate_kernels.py +++ b/packages/microcosm-build/src/microcosm/build/transport/gate_kernels.py @@ -721,6 +721,8 @@ def implementation_hash(self) -> str: artifact_types, binding_identity, target_kernels, + target_kernels.reference_profiles_module, + target_kernels.reference_selection_module, gate_battery_module, gates_module, country_spec_module, diff --git a/packages/microcosm-build/src/microcosm/build/transport/geography_kernels.py b/packages/microcosm-build/src/microcosm/build/transport/geography_kernels.py index 759957a82b..79edaa0db4 100644 --- a/packages/microcosm-build/src/microcosm/build/transport/geography_kernels.py +++ b/packages/microcosm-build/src/microcosm/build/transport/geography_kernels.py @@ -281,6 +281,8 @@ def implementation_hash(self) -> str: sys.modules[__name__], atomic_geography_module, target_kernels, + target_kernels.reference_profiles_module, + target_kernels.reference_selection_module, graph_inputs, registry_module, hierarchy_module, diff --git a/packages/microcosm-build/src/microcosm/build/transport/receipt_contract.py b/packages/microcosm-build/src/microcosm/build/transport/receipt_contract.py index 9227d8803c..4c71ad2276 100644 --- a/packages/microcosm-build/src/microcosm/build/transport/receipt_contract.py +++ b/packages/microcosm-build/src/microcosm/build/transport/receipt_contract.py @@ -5,7 +5,10 @@ import itertools import json import math +from collections import deque from collections.abc import Mapping +from fractions import Fraction +from numbers import Real import numpy as np @@ -155,6 +158,12 @@ def validate_receipt_v2(contract): raise ValueError( f"Receipt non-person {field} reserved for the declared-unit receipt extension." ) + if ( + item["output"] == f"{item['projection']}_id" + or item["output"].startswith("person_") + and item["output"].endswith("_id") + ): + raise ValueError("Receipt output cannot be a structural column.") if "strata" in row: if not isinstance(row["strata"], list | tuple) or not row["strata"]: raise ValueError("Receipt strata must be a non-empty list.") @@ -203,6 +212,7 @@ def validate_receipt_v2(contract): raise ValueError( "Each receipt exclusion group names distinct declared outputs." ) + _ordered_receipt_outputs([row["output"] for row in parsed], groups) unmodelled = _strings( contract.get("unmodelled_programs", []), "unmodelled_programs" ) @@ -225,58 +235,129 @@ def _ratio(target, mass): return target / mass if mass else (0 if target == 0 else None) +def _ordered_receipt_outputs(outputs, groups): + pending, ordered = list(outputs), [] + while pending: + available = [ + output + for output in pending + if all( + not any(prior in pending for prior in group[: group.index(output)]) + for group in groups + if output in group + ) + ] + if not available: + raise ValueError("Receipt exclusion groups declare a cyclic order.") + output = available[0] + ordered.append(output) + pending.remove(output) + return ordered + + def hall_diagnostics(demands, weights): - """Check every subset, using fsum and a declared relative tolerance. + """Report one maximally deficient demand set using exact rational max-flow. A demand is (programme, filter, target, eligibility mask). Split people into - equal-neighbourhood weighted supplies. A network joins source to supplies, - supplies to eligible demands, and demands to sink with target capacities. - The max-flow/min-cut theorem says all targets can be filled iff every subset - of demands has total target at most its neighbouring supply (weighted Hall). - Dividing a flow by each person's weight gives mutually exclusive receipt - probabilities, and multiplying such probabilities gives a flow, proving + equal-neighbourhood weighted supplies. Join source to supplies, supplies to + eligible demands, and demands to sink. Convert each float weight, target and + relative tolerance r with Fraction, and scale demand capacities by (1 - r). + For T > N, the relative Hall rule permits a subset iff N >= (1 - r) * T. + Thus max-flow saturates every demand iff all subsets satisfy that rule. + + The demand nodes on the sink side of a minimum cut maximise (1 - r) * T - N. + When infeasible, report this one set with its unscaled target and supply. + Edmonds-Karp uses O(V * E**2) rational operations, never demand subsets. + A flow divided by people's weights gives mutually exclusive receipt + probabilities, and multiplying those probabilities gives a flow, proving equivalence for expected counts. This is diagnostic only; draws and priority may still refuse even when the fractional allocation is feasible. """ - # Collapse identical neighbourhoods once. Each subsequent cut reads only - # these weighted supplies, rather than scanning the person table again. + grouped_weights = {} if demands: packed = np.packbits( np.column_stack([item[3] for item in demands]), axis=1, bitorder="little" ) - grouped_weights = {} for packed_pattern, weight in zip(packed, weights, strict=True): pattern = int.from_bytes(packed_pattern.tobytes(), "little") if pattern: - grouped_weights.setdefault(pattern, []).append(float(weight)) - supplies = [ - (pattern, math.fsum(grouped_weights[pattern])) - for pattern in sorted(grouped_weights) - ] - else: - supplies = [] + grouped_weights[pattern] = grouped_weights.get( + pattern, Fraction() + ) + Fraction(float(weight)) + supplies = sorted(grouped_weights.items()) + scale = 1 - Fraction(RECEIPT_RELATIVE_TOLERANCE) + targets = [Fraction(item[2]) for item in demands] + required = sum(targets, Fraction()) * scale + source, supply_start = 0, 1 + demand_start = supply_start + len(supplies) + sink = demand_start + len(demands) + graph = [[] for _ in range(sink + 1)] + + def edge(left, right, capacity): + graph[left].append([right, capacity, len(graph[right])]) + graph[right].append([left, Fraction(), len(graph[left]) - 1]) + + for index, (pattern, mass) in enumerate(supplies): + supply = supply_start + index + edge(source, supply, mass) + for demand in range(len(demands)): + if pattern & (1 << demand): + edge(supply, demand_start + demand, required) + for index, target in enumerate(targets): + edge(demand_start + index, sink, scale * target) + flow = Fraction() + while True: + parents = {source: None} + queue = deque([source]) + while queue and sink not in parents: + left = queue.popleft() + for index, (right, capacity, _) in enumerate(graph[left]): + if capacity > 0 and right not in parents: + parents[right] = (left, index) + queue.append(right) + if sink not in parents: + break + capacity, right = required, sink + while right != source: + left, index = parents[right] + capacity = min(capacity, graph[left][index][1]) + right = left + right = sink + while right != source: + left, index = parents[right] + arc = graph[left][index] + arc[1] -= capacity + graph[right][arc[2]][1] += capacity + right = left + flow += capacity violations = [] - for size in range(1, len(demands) + 1): - for indices in itertools.combinations(range(len(demands)), size): - subset = [demands[index] for index in indices] - subset_pattern = sum(1 << index for index in indices) - total = math.fsum(item[2] for item in subset) - mass = math.fsum( - supply for pattern, supply in supplies if pattern & subset_pattern - ) - if total > mass and not math.isclose( - total, mass, rel_tol=RECEIPT_RELATIVE_TOLERANCE, abs_tol=0 - ): - filters = {item[1] for item in subset} - violations.append( - { - "programs": [item[0] for item in subset], - "filter": next(iter(filters)) if len(filters) == 1 else None, - "filters": [item[1] for item in subset], - "target": total, - "eligible_mass": mass, - } - ) + if flow < required: + indices = [ + index + for index in range(len(demands)) + if demand_start + index not in parents + ] + subset = [demands[index] for index in indices] + subset_pattern = sum(1 << index for index in indices) + filters = {item[1] for item in subset} + violations.append( + { + "programs": [item[0] for item in subset], + "filter": next(iter(filters)) if len(filters) == 1 else None, + "filters": [item[1] for item in subset], + "target": float(sum((targets[index] for index in indices), Fraction())), + "eligible_mass": float( + sum( + ( + mass + for pattern, mass in supplies + if pattern & subset_pattern + ), + Fraction(), + ) + ), + } + ) return { "violations": violations, "feasible": not violations, @@ -284,6 +365,17 @@ def hall_diagnostics(demands, weights): } +def _refuse_assignment(message, audit, out): + audit["status"] = "refused" + for group_audit in audit["exclusion_groups"]: + group_audit["status"] = ( + "completed" + if all(name in out for name in group_audit["order"]) + else "refused" + ) + raise ReceiptAssignmentRefused(message, audit) + + def assign_receipts_v2( person, contract, @@ -314,11 +406,31 @@ def assign_receipts_v2( or (weights < 0).any() ): raise ValueError("Receipt weights must be aligned, finite and non-negative.") + try: + total_weight = math.fsum(weights.tolist()) + except OverflowError as error: + raise ValueError("Receipt weights must have a finite sum.") from error + if not math.isfinite(total_weight): + raise ValueError("Receipt weights must have a finite sum.") tables = ( dict(tables) if tables is not None else {row[field]: person for row in rows for field in RECEIPT_UNIT_FIELDS} ) + for entity in {row[field] for row in rows for field in RECEIPT_UNIT_FIELDS}: + table = tables.get(entity) + id_column = f"{entity}_id" + if ( + table is None + or len(table) != len(person) + or id_column not in table + or not np.array_equal( + table[id_column].to_numpy(), person[id_column].to_numpy() + ) + ): + raise ValueError( + "Receipt entity tables must align with person length and ids." + ) eligible, masks, target_values = {}, {}, {} before_payment = {} for row in rows: @@ -355,12 +467,15 @@ def assign_receipts_v2( f"Receipt target {name!r} needs a surface and weights." ) value = targets[name] - if ( - isinstance(value, bool) - or not isinstance(value, int | float) - or not math.isfinite(value) - or value < 0 - ): + if isinstance(value, bool | np.bool_) or not isinstance(value, Real): + raise ValueError("Receipt target must be finite and non-negative.") + try: + value = float(value) + except (OverflowError, ValueError) as error: + raise ValueError( + "Receipt target must be finite and non-negative." + ) from error + if not math.isfinite(value) or value < 0: raise ValueError("Receipt target must be finite and non-negative.") target_values[name] = value if filter_name is None: @@ -422,8 +537,7 @@ def assign_receipts_v2( for row in group_rows for s in receipt_strata(row) ] - # This includes each stratum's 2**k - 1 programme subsets and the - # cross-filter cuts required when a group also has unstratified demands. + # One flow includes all strata and mixed-filter demand cuts. feasibility = hall_diagnostics(demands, weights) audit["exclusion_groups"].append( { @@ -434,23 +548,9 @@ def assign_receipts_v2( } ) by_output = {row["output"]: row for row in rows} - pending = list(by_output) - ordered = [] - while pending: - available = [ - output - for output in pending - if all( - not any(prior in pending for prior in group[: group.index(output)]) - for group in groups - if output in group - ) - ] - if not available: - raise ValueError("Receipt exclusion groups declare a cyclic order.") - output = available[0] - ordered.append(by_output[output]) - pending.remove(output) + ordered = [ + by_output[output] for output in _ordered_receipt_outputs(by_output, groups) + ] id_column = f"{rows[0]['projection']}_id" out = person.loc[:, [id_column]].copy() for row in ordered: @@ -510,16 +610,10 @@ def assign_receipts_v2( } stratum_audits.append(detail) if target > mass: - audit["status"] = "refused" - for group_audit in audit["exclusion_groups"]: - group_audit["status"] = ( - "completed" - if all(name in out for name in group_audit["order"]) - else "refused" - ) - raise ReceiptAssignmentRefused( + _refuse_assignment( f"Receipt target {name!r} exceeds eligible weighted mass in {order} order.", audit, + out, ) chosen = pool & (draws < rate) assigned |= chosen @@ -540,6 +634,12 @@ def assign_receipts_v2( }, } ) + if not detail["numerical_check"]["passed"]: + _refuse_assignment( + f"Receipt target {name!r} failed the numerical expected-mass check in {order} order.", + audit, + out, + ) audit["programs"][output]["realized_mass"] = _mass(weights, assigned) audit["programs"][output]["n_receipts"] = int(assigned.sum()) out[output] = assigned diff --git a/packages/microcosm-build/src/microcosm/build/transport/target_kernels.py b/packages/microcosm-build/src/microcosm/build/transport/target_kernels.py index 2b95a01d76..6e29496e0e 100644 --- a/packages/microcosm-build/src/microcosm/build/transport/target_kernels.py +++ b/packages/microcosm-build/src/microcosm/build/transport/target_kernels.py @@ -966,6 +966,8 @@ def implementation_hash(self) -> str: hierarchy_module, calibrate_artifacts_module, reference_intervals_module, + reference_profiles_module, + reference_selection_module, *_MATRIX_MODULES, dependencies=self.capabilities.dependencies, ) diff --git a/packages/microcosm-build/src/microcosm/build/transport/terminal_kernels.py b/packages/microcosm-build/src/microcosm/build/transport/terminal_kernels.py index fb699fff17..bf1539cdf5 100644 --- a/packages/microcosm-build/src/microcosm/build/transport/terminal_kernels.py +++ b/packages/microcosm-build/src/microcosm/build/transport/terminal_kernels.py @@ -203,6 +203,8 @@ def implementation_hash(self) -> str: graph_inputs, artifact_types, target_kernels, + target_kernels.reference_profiles_module, + target_kernels.reference_selection_module, calibrate_artifacts_module, calibrate_diagnostics_module, diagnostics_schema_module, diff --git a/packages/microcosm-build/tests/engine_free/shared/test_transport_receipt_v2.py b/packages/microcosm-build/tests/engine_free/shared/test_transport_receipt_v2.py index f3786058e7..dbcc5aa360 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_transport_receipt_v2.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_transport_receipt_v2.py @@ -10,11 +10,12 @@ import copy import json import math +from fractions import Fraction import numpy as np import pandas as pd import pytest -from hypothesis import HealthCheck, given, settings +from hypothesis import HealthCheck, example, given, settings from hypothesis import strategies as st from microcosm.build.transport import receipt_contract as receipt_module @@ -673,12 +674,16 @@ def test_contract_refuses_a_draw_key_collision(monkeypatch): @PROPERTY @given( st.lists( - st.tuples(st.text(max_size=12), st.text(max_size=12)), + st.tuples( + st.text(alphabet='a:/|"[]\\', max_size=12), + st.text(alphabet='a:/|"[]\\', max_size=12), + ), min_size=1, max_size=15, unique=True, ) ) +@example(pairs=[("a:b", "c"), ("a", "b:c"), ("", ":"), (":", "")]) def test_property_draw_key_is_injective(pairs): assert len({receipt_draw_key(*pair) for pair in pairs}) == len(pairs) @@ -765,8 +770,9 @@ def test_property_disjoint_strata_and_outside_partition_eligible_mass(ages, data ) -@PROPERTY +@settings(max_examples=18, deadline=None, database=None, derandomize=True) @given(st.integers(1, 15), st.floats(min_value=0, max_value=0.25, allow_nan=False)) +@example(count=15, fraction=0.25) def test_property_exclusion_group_never_assigns_two_flags(count, fraction): contract = _contract( _program("alpha"), @@ -942,3 +948,173 @@ def test_hall_diagnostic_checks_mixed_unstratified_and_stratified_subsets(): and violation["eligible_mass"] == 2 for violation in feasibility["violations"] ) + + +@st.composite +def _hall_cases(draw): + count = draw(st.integers(0, 12)) + size = draw(st.integers(0, 8)) + # Binary fractions retain exact, varied float capacities without rounding + # away the comparison against the independent rational reference below. + capacity = st.integers(0, 32).map(lambda value: value / 8) + weights = np.asarray(draw(st.lists(capacity, min_size=count, max_size=count))) + demands = [ + ( + f"p{index}", + f"f{index % 3}", + draw(capacity), + np.asarray( + draw(st.lists(st.booleans(), min_size=count, max_size=count)), + dtype=bool, + ), + ) + for index in range(size) + ] + return demands, weights + + +@settings(max_examples=120, deadline=None, database=None, derandomize=True) +@given(_hall_cases()) +def test_property_hall_flow_matches_exact_bruteforce(case): + demands, weights = case + scale = 1 - Fraction(receipt_module.RECEIPT_RELATIVE_TOLERANCE) + + def deficiency(indices): + total = sum((Fraction(demands[index][2]) for index in indices), Fraction()) + mask = np.zeros(len(weights), dtype=bool) + for index in indices: + mask |= demands[index][3] + mass = sum((Fraction(float(value)) for value in weights[mask]), Fraction()) + return scale * total - mass + + maximum = max( + ( + deficiency([i for i in range(len(demands)) if subset & (1 << i)]) + for subset in range(1, 1 << len(demands)) + ), + default=Fraction(), + ) + result = receipt_module.hall_diagnostics(demands, weights) + assert result["feasible"] == (maximum <= 0) + if result["feasible"]: + assert result["violations"] == [] + else: + (violation,) = result["violations"] + indices = [ + i for i, demand in enumerate(demands) if demand[0] in violation["programs"] + ] + assert deficiency(indices) == maximum + assert violation["target"] == float( + sum((Fraction(demands[i][2]) for i in indices), Fraction()) + ) + union = np.logical_or.reduce([demands[i][3] for i in indices]) + assert violation["eligible_mass"] == float( + sum((Fraction(float(value)) for value in weights[union]), Fraction()) + ) + assert violation["filters"] == [demands[i][1] for i in indices] + + +def test_hall_exact_relative_tolerance_boundary(monkeypatch): + monkeypatch.setattr(receipt_module, "RECEIPT_RELATIVE_TOLERANCE", 0.125) + demands = [("alpha", None, 8.0, np.ones(1, dtype=bool))] + assert receipt_module.hall_diagnostics(demands, np.array([7.0]))["feasible"] + assert not receipt_module.hall_diagnostics( + demands, np.array([np.nextafter(7.0, 0)]) + )["feasible"] + + +def test_hall_24_demands_collapses_supplies_without_enumerating_subsets(monkeypatch): + def no_subsets(*args, **kwargs): + raise AssertionError("Hall diagnostics must not enumerate demand subsets") + + monkeypatch.setattr(receipt_module.itertools, "combinations", no_subsets) + people = np.arange(20_000) + demands = [ + (f"p{program}", f"s{stratum}", 1.0, people % 6 == stratum) + for program in range(4) + for stratum in range(6) + ] + result = receipt_module.hall_diagnostics(demands, np.ones(len(people))) + assert result["feasible"] + assert result["violations"] == [] + + +def test_numerical_check_underflow_refuses_with_audit(): + with pytest.raises( + ReceiptAssignmentRefused, match="numerical expected-mass check" + ) as refused: + _assign( + _people((20, 21, 22)), weights=[1e200] * 3, targets={"alpha_total": 1e-200} + ) + audit = refused.value.audit + assert audit["status"] == "refused" + detail = audit["programs"]["receives_alpha"]["strata"][0] + assert detail["numerical_check"]["passed"] is False + assert detail["numerical_check"]["expected_mass"] == 0 + assert detail["target"] == 1e-200 + + +@pytest.mark.parametrize("weight", [math.inf, -math.inf, math.nan]) +def test_v2_nonfinite_weights_refuse(weight): + with pytest.raises(ValueError, match="finite"): + _assign(_people((20,)), weights=[weight]) + + +def test_v2_overflowing_weight_sum_refuses_as_value_error(): + with pytest.raises(ValueError, match="finite sum"): + _assign(_people((20, 21, 22)), weights=[1e308] * 3) + + +def test_v2_cyclic_exclusion_groups_refuse_at_validation(): + contract = _contract( + _program("alpha"), + _program("beta"), + groups=[ + ["receives_alpha", "receives_beta"], + ["receives_beta", "receives_alpha"], + ], + ) + with pytest.raises(ValueError, match="cyclic order"): + validate_receipt_contract(contract) + + +@pytest.mark.parametrize("output", ["person_id", "person_household_id"]) +def test_v2_output_structural_column_refuses_in_pure_function(output): + row = _program() + row["output"] = output + with pytest.raises(ValueError, match="structural column"): + _assign(_people((20,)), _contract(row)) + + +@pytest.mark.parametrize("mismatch", ["length", "ids", "order", "missing"]) +def test_v2_person_table_must_match_argument(mismatch): + person = _people((20, 21)) + table = person.copy() + if mismatch == "length": + table = table.iloc[:1] + elif mismatch == "ids": + table["person_id"] += 10 + else: + table = table.iloc[::-1] + with pytest.raises(ValueError, match="align with person length and ids"): + receipt_module.assign_receipts_v2( + person, + _contract(), + weights=np.ones(2), + targets={"alpha_total": 0}, + knobs={"assignment": "central"}, + tables={} if mismatch == "missing" else {"person": table}, + ) + + +@pytest.mark.parametrize("target", [np.int64(1), np.float32(1), np.float64(1)]) +def test_v2_numpy_real_targets_are_accepted(target): + out, audit = _assign(_people((20,)), targets={"alpha_total": target}) + assert out["receives_alpha"].tolist() == [True] + assert type(audit["programs"]["receives_alpha"]["strata"][0]["target"]) is float + + +@pytest.mark.parametrize("target", [True, np.bool_(True)]) +def test_v2_boolean_targets_refuse(target): + with pytest.raises(ValueError, match="target must be finite"): + _assign(_people((20,)), targets={"alpha_total": target}) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_transport_receipt_v2_kernel.py b/packages/microcosm-build/tests/engine_free/shared/test_transport_receipt_v2_kernel.py index c391394f77..fddf3d6b00 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_transport_receipt_v2_kernel.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_transport_receipt_v2_kernel.py @@ -2,6 +2,7 @@ import copy import json +from dataclasses import replace from types import SimpleNamespace import numpy as np @@ -186,3 +187,23 @@ def test_v2_kernel_resolves_profile_from_declared_knob(tmp_path): ) with pytest.raises(ValueError, match="assignment_knob"): TAKEUP_ASSIGN.run(context) + + +@pytest.mark.parametrize( + "missing", ["calibration_bindings", "calibration_bindings_sha256"] +) +def test_v2_kernel_requires_bindings_and_sha_together(tmp_path, missing): + # No strata need bindings here: a lone SHA must refuse on its own, rather + # than falling through to the separate missing-stratum-bindings refusal. + context = _context(tmp_path, filter_name=None) + params = dict(context.params) + contract = json.loads(params["receipt_contract"]) + row = contract["programs"][0] + del row["strata"] + del row["outside_strata"] + row["target"] = "alpha_target" + params["receipt_contract"] = canonical_text(contract) + params["receipt_contract_sha256"] = digest(contract) + del params[missing] + with pytest.raises(ValueError, match="supplied together"): + TAKEUP_ASSIGN.run(replace(context, params=params)) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_transport_reference_selection.py b/packages/microcosm-build/tests/engine_free/shared/test_transport_reference_selection.py index b24b34d4df..110f630a64 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_transport_reference_selection.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_transport_reference_selection.py @@ -3,8 +3,11 @@ from __future__ import annotations import ast +import importlib +import inspect import json from copy import deepcopy +from types import ModuleType import numpy as np import pandas as pd @@ -193,7 +196,7 @@ def _v2_spec(): spec["resources"]["calibration_bindings"] = { "schema_version": 1, "required_inputs": {"person": ["age"]}, - "input_domains": {"person": {"age": {"kind": "integer", "minimum": 0}}}, + "input_domains": {"person": {"age": {"kind": "nonnegative_integer"}}}, "columns": [ { "entity": "person", @@ -636,6 +639,208 @@ def test_legacy_selection_path_changed_mutant_frozen_surface_params_and_keys( assert keys[0] == keys[1] +@pytest.mark.parametrize( + ("module_name", "kernel_name"), + [ + ("column_kernels", "TAKEUP_ASSIGN"), + ("gate_kernels", "GateBatteryKernel"), + ("geography_kernels", "GEOGRAPHY_SUPPORT_FROM_FACTS"), + ("population_kernels", "TRANSPORT_CREATE"), + ("population_kernels", "TRANSPORT_QUANTILE_MAP"), + ("takeup_kernels", "TAKEUP_GAP"), + ("target_kernels", "TARGETS_COMPILE"), + ("target_kernels", "TARGETS_PROBLEM"), + ("target_kernels", "TAKEUP_COMPARE"), + ("terminal_kernels", "DIAGNOSTICS_CALIBRATION"), + ], +) +def test_surface_consumers_hash_selection_and_profile_modules( + monkeypatch, module_name, kernel_name +): + # source_hash reads only each explicitly supplied module's bytes. Cover + # every kernel reaching decode_target_surface, including through helpers. + module = importlib.import_module(f"microcosm.build.transport.{module_name}") + hashed = set() + + def capture(*objects, **kwargs): + for obj in objects: + defining_module = ( + obj if isinstance(obj, ModuleType) else inspect.getmodule(obj) + ) + hashed.add(defining_module.__name__) + return SHA + + monkeypatch.setattr(module, "source_hash", capture) + kernel = getattr(module, kernel_name) + if kernel_name == "GateBatteryKernel": + kernel = kernel({}) + kernel.implementation_hash() + assert { + "microcosm.build.reference_profiles", + "microcosm.build.transport.reference_selection", + } <= hashed + + +def _compiled_surface_with_exemption(tmp_path): + facts = write_facts( + tmp_path / "facts.jsonl", [toy_fact("used", 3, entity="person")] + ) + payload, _ = target_kernels.compile_target_surface( + facts, + _document(), + country="xx", + references_sha256=SHA, + reference_selection=_selection(), + ) + return payload + + +def test_surface_decode_refuses_active_exemption(tmp_path): + document = json.loads(_compiled_surface_with_exemption(tmp_path)) + document["reference_selection"] = _selection(_exemption(status="active")) + with pytest.raises(ValueError, match="cannot exempt an active reference"): + target_kernels.decode_target_surface(canonical_json(document)) + + +def test_surface_decode_refuses_exempt_compiled_reference(tmp_path): + document = json.loads(_compiled_surface_with_exemption(tmp_path)) + document["reference_selection"] = _selection(_exemption("used")) + with pytest.raises(ValueError, match="An exempt reference entered"): + target_kernels.decode_target_surface(canonical_json(document)) + + +def test_resolution_filters_transformed_exempt_reference(tmp_path): + from microcosm.build.reference_profiles import ( + reference_document_sha256, + resolve_reference_profiles, + ) + + def selector(key, category=None): + value = { + "aggregate_fact_key": key, + "source_name": "publisher", + "source_measure_id": "count", + "source_concept": "people", + "entity_name": "person", + "record_set_id": "people", + "period_type": "calendar_year", + "period_value": 2023, + "geography_level": "region", + "geography_id": "a", + "geography_vintage": "v", + "dimensions": [] if category is None else ["category"], + } + if category is not None: + value["dimension_values"] = {"category": category} + return value + + document = _document() + parked = document["target_references"][1] + parked.pop("ledger_fact_key", None) + parked["ledger_selector"] = selector("atom", 1) + transformation = { + "reference_names": ["parked"], + "operation": "allocated_residual_by_ratio", + "value_operands": [ + {"role": "base", "expected_member_count": 1}, + {"role": "total", **selector("total")}, + { + "role": "partition", + "member_selectors": [selector("atom", 1)], + "allocation_member_keys": ["atom"], + }, + ], + "metadata": {"allocation_ruling": "Declared allocation."}, + } + declaration = { + "schema_version": 1, + "dimensions": { + "allocation": { + "default": "allocated", + "profiles": {"allocated": {"transformations": [transformation]}}, + } + }, + } + effective, _, receipt = resolve_reference_profiles(document, declaration, {}) + assert receipt["transformed_names"] == ["parked"] + facts = write_facts( + tmp_path / "facts.jsonl", [toy_fact("used", 3, entity="person")] + ) + payload, registry = target_kernels.compile_target_surface( + facts, + effective, + country="xx", + references_sha256=reference_document_sha256(effective), + reference_resolution=receipt, + reference_selection=_selection(), + ) + surface = target_kernels.decode_target_surface(payload) + assert [spec.name for spec in registry.specs] == ["used"] + assert surface.reference_resolution == receipt + assert surface.references_sha256 == receipt["effective_references_sha256"] + + +def test_v2_activation_refuses_unsupported_calibration_input_domain(): + spec, _ = _v2_spec() + domain = spec["resources"]["calibration_bindings"]["input_domains"]["person"]["age"] + domain["kind"] = "integer" + with pytest.raises( + ValueError, match="Calibration input person.age has an unsupported domain" + ): + compose.validate_transport_activation(spec) + + +@pytest.mark.parametrize("knob_scope", ["central", "scenario"]) +def test_v2_assignment_node_key_uses_only_assignment_knob(knob_scope): + spec, assignment = _v2_spec() + spec["resources"]["scenarios"]["central_knobs"]["unrelated"] = "before" + changed = deepcopy(spec) + before_selection = {"unrelated": "before"} if knob_scope == "scenario" else {} + after_selection = {"unrelated": "after"} if knob_scope == "scenario" else {} + if knob_scope == "central": + changed["resources"]["scenarios"]["central_knobs"]["unrelated"] = "after" + + def assignment_key(selected_spec, selection): + node = compose._node( + selected_spec, assignment, CONFIG, {}, profile_selection=selection + ) + graph = Graph( + "xx", + (SourceRef("seed", "raw-bytes-v1"),), + ( + Node( + "open", + "fixture.create@1", + sources=("seed",), + structural=StructuralDelta.CREATE, + outputs=CONFIG.create_outputs, + ), + node, + ), + ) + return node_key( + compile_graph(graph), + node.id, + {"open": SHA}, + SHA, + {}, + kernel_capabilities=Capabilities(Determinism.DETERMINISTIC), + ) + + baseline_key = assignment_key(spec, before_selection) + assert baseline_key == assignment_key(changed, after_selection) + assert baseline_key != assignment_key( + changed, {**after_selection, "receipt_profile": "reverse"} + ) + + +def test_v2_assignment_node_refuses_unresolved_assignment_knob(): + spec, assignment = _v2_spec() + del spec["resources"]["scenarios"]["central_knobs"]["receipt_profile"] + with pytest.raises(ValueError, match="'receipt_profile' is not a resolved knob"): + compose._node(spec, assignment, CONFIG, {}, profile_selection={}) + + def test_full_effective_resolution_hash_survives_exemption_filtering(tmp_path): from microcosm.build.reference_profiles import ( reference_document_sha256, @@ -665,3 +870,18 @@ def test_full_effective_resolution_hash_survives_exemption_filtering(tmp_path): surface = target_kernels.decode_target_surface(payload) assert surface.references_sha256 == receipt["effective_references_sha256"] assert surface.reference_resolution == receipt + + +def test_v2_activation_refuses_cyclic_exclusion_groups(): + spec, _ = _v2_spec() + contract = spec["resources"]["receipt_contract"]["receipts"] + second = deepcopy(contract["programs"][0]) + second.update(program="second", output="receives_second") + second["strata"][0]["target"] = "second_target" + contract["programs"].append(second) + contract["exclusion_groups"] = [ + ["receives_benefit", "receives_second"], + ["receives_second", "receives_benefit"], + ] + with pytest.raises(ValueError, match="cyclic order"): + compose.validate_transport_activation(spec) From 84c2082b9d9c38a6b0e9e64f3789afcb8acf2747 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 26 Oct 2026 05:30:09 -0700 Subject: [PATCH 17/24] Let the NZ hold-out property test tolerate a stalled host On 2026-10-26 a review run of test_nz_spec_package.py failed one test with Hypothesis's too_slow health check: drawing one of two strings took 36.9 s while the shared host's load average was about 300. The strategy is cheap, so the failure measured the host, not the test. The hold-out property test now suppresses too_slow, keeps its 300 examples and has no deadline, as before. Co-Authored-By: Claude Opus 5.5 --- .../tests/engine_free/shared/test_nz_spec_package.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py b/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py index d8ed8bcfe3..2e2389b246 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_nz_spec_package.py @@ -155,7 +155,7 @@ from urllib.parse import unquote, urlsplit import pytest -from hypothesis import assume, given, settings +from hypothesis import HealthCheck, assume, given, settings from hypothesis import strategies as st from microcosm.build.chronicle_epoch import ( @@ -1377,7 +1377,13 @@ def test_no_held_out_fact_reaches_calibration_or_pre_calibration(self) -> None: ) assert HELD_OUT_FAMILIES.isdisjoint(reference.family for reference in upstream) - @settings(max_examples=300, deadline=None) + # A stalled shared host once made one draw take 36.9 s (load1 ~300), which + # Hypothesis reports as too_slow; the strategy itself is cheap. + @settings( + max_examples=300, + deadline=None, + suppress_health_check=[HealthCheck.too_slow], + ) @given( data=st.data(), kind=st.sampled_from(HOLDOUT_LEAK_KINDS), From 97c08e0f3550bebb71ae4ae3b69328021e4d538f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 26 Oct 2026 09:11:04 -0700 Subject: [PATCH 18/24] Check receipt unit views at the dispatch seam, by membership id Belgium's follow-up to review r1 (its f10-followup.diff, committed unchanged; sha256 of the diff de15bc7a87dc17f9a7f15b8e0e072006cb20808f89e2cc9e63a64252d3817559). At 84c2082b9 the entity-table check compared each raw unit table's `{entity}_id` with `person["{entity}_id"]`, before `receipt_dispatch_tables`. That broke Belgium's 10/24 widen-only condition for its declared-unit extension: with `payee` widened to "household", a person-aligned household view raised `KeyError: 'household_id'`, and a native household table broadcast at the seam was refused as misaligned. Belgium's independent Sol review (subfleet run 20261026-052510-nz1397-r1-review) found it, and Belgium reproduced both cases. `_unit_views_align` now checks what the [BE] dispatch seam returns for each row. It matches each unit's ids through the person table's membership column (`person_id` for person, `person_{entity}_id` otherwise). Every unit field is still held at "person" today, so the check still compares the person table with itself; only the widened case changes. Test: test_declared_unit_extension_only_widens_values[view, seam] widens `payee` to household both ways and requires the assignment to go through. Hub run on this tree, one file per process at -n 0: test_transport_receipt_v2.py 99 passed (the 2 new cases among them; 2 passed verbose on their own) and test_transport_receipt_v2_kernel.py 14 passed. Co-Authored-By: Claude Opus 5.5 --- .../build/transport/receipt_contract.py | 39 ++++++++++------- .../shared/test_transport_receipt_v2.py | 42 +++++++++++++++++++ 2 files changed, 67 insertions(+), 14 deletions(-) diff --git a/packages/microcosm-build/src/microcosm/build/transport/receipt_contract.py b/packages/microcosm-build/src/microcosm/build/transport/receipt_contract.py index 4c71ad2276..1b98b9e028 100644 --- a/packages/microcosm-build/src/microcosm/build/transport/receipt_contract.py +++ b/packages/microcosm-build/src/microcosm/build/transport/receipt_contract.py @@ -376,6 +376,29 @@ def _refuse_assignment(message, audit, out): raise ReceiptAssignmentRefused(message, audit) +def _unit_views_align(tables, row, person): + """Check what the [BE] dispatch seam returns, not the raw unit tables. + + Each unit's ids are matched through the person table's membership column, so + a declared unit broadcast to person rows at the seam aligns. + """ + if any(row[field] not in tables for field in RECEIPT_UNIT_FIELDS): + return False + for field, view in receipt_dispatch_tables(tables, row).items(): + entity = row[field] + membership = "person_id" if entity == "person" else f"person_{entity}_id" + if ( + len(view) != len(person) + or f"{entity}_id" not in view + or membership not in person + or not np.array_equal( + view[f"{entity}_id"].to_numpy(), person[membership].to_numpy() + ) + ): + return False + return True + + def assign_receipts_v2( person, contract, @@ -417,20 +440,8 @@ def assign_receipts_v2( if tables is not None else {row[field]: person for row in rows for field in RECEIPT_UNIT_FIELDS} ) - for entity in {row[field] for row in rows for field in RECEIPT_UNIT_FIELDS}: - table = tables.get(entity) - id_column = f"{entity}_id" - if ( - table is None - or len(table) != len(person) - or id_column not in table - or not np.array_equal( - table[id_column].to_numpy(), person[id_column].to_numpy() - ) - ): - raise ValueError( - "Receipt entity tables must align with person length and ids." - ) + if not all(_unit_views_align(tables, row, person) for row in rows): + raise ValueError("Receipt entity tables must align with person length and ids.") eligible, masks, target_values = {}, {}, {} before_payment = {} for row in rows: diff --git a/packages/microcosm-build/tests/engine_free/shared/test_transport_receipt_v2.py b/packages/microcosm-build/tests/engine_free/shared/test_transport_receipt_v2.py index dbcc5aa360..295da454f7 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_transport_receipt_v2.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_transport_receipt_v2.py @@ -1107,6 +1107,48 @@ def test_v2_person_table_must_match_argument(mismatch): ) +@pytest.mark.parametrize("design", ["view", "seam"]) +def test_declared_unit_extension_only_widens_values(monkeypatch, design): + """Widening a reserved unit, as BE's extension will, needs no rewrite here. + + ``payee`` becomes household after validation. Either a person-aligned + household view, or a native household table broadcast at the dispatch seam, + must assign, matching ids through ``person_household_id``. + """ + validate = receipt_module.validate_receipt_v2 + dispatch = receipt_module.receipt_dispatch_tables + person = _people((20, 21, 22)).drop(columns="payment") + person["person_household_id"] = [10, 10, 20] + household = pd.DataFrame({"household_id": [10, 20], "payment": [1.0, 1.0]}) + + def to_people(table): + members = person["person_household_id"].to_numpy() + return table.set_index("household_id").loc[members].reset_index() + + def widened(contract): + seed, rows, groups = validate(contract) + return seed, [{**row, "payee": "household"} for row in rows], groups + + def broadcast(tables, row): + return {**dispatch(tables, row), "payee": to_people(tables["household"])} + + monkeypatch.setattr(receipt_module, "validate_receipt_v2", widened) + if design == "seam": + monkeypatch.setattr(receipt_module, "receipt_dispatch_tables", broadcast) + tables = {"person": person, "household": household} + else: + tables = {"person": person, "household": to_people(household)} + out, _ = receipt_module.assign_receipts_v2( + person, + _contract(), + weights=np.ones(3), + targets={"alpha_total": 3}, + knobs={"assignment": "central"}, + tables=tables, + ) + assert out["receives_alpha"].tolist() == [True, True, True] + + @pytest.mark.parametrize("target", [np.int64(1), np.float32(1), np.float64(1)]) def test_v2_numpy_real_targets_are_accepted(target): out, audit = _assign(_people((20,)), targets={"alpha_total": target}) From 2f2f7041efe105a1219b49746bebf9da4753b5c6 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 26 Oct 2026 13:54:03 -0700 Subject: [PATCH 19/24] Bound the Hall diagnostic with an exact integer Dinic; refuse two v2 gaps Answers review r2 (Claude Opus 5.5, REQUEST_CHANGES at 84c2082b9) F-A, F-C and F-D, Belgium's items (a), (b) and (c) on r1, and reviews r3a and r3b (GPT-6.1 Sol, both REQUEST_CHANGES on this change's first draft). Review r4 (GPT-6.1 Sol) approved this tree with one test nit, added below. F-A, Belgium (c): hall_diagnostics was unbounded in practice. With independent random eligibility masks, almost every person has a pattern of their own, and Edmonds-Karp over Fractions ran for minutes. The 0.11 s figure in 51080462's message came from a fixture with 6 distinct patterns. Now: - each weight is a float, so it is an exact multiple of one power-of-two denominator; each pattern's weights are summed as integers over it, 65,536 people at a time (_HALL_WEIGHT_CHUNK), not one Fraction add per person; - people eligible for no demand join no supply, and their weights are never read, as at 84c2082b9; - every capacity is an integer over the lcm of that denominator and the scaled targets' denominators (a Fraction target need not be dyadic); - Dinic (BFS levels, then a blocking flow with current-arc pointers) replaces Edmonds-Karp. A shortest augmenting path alternates supplies with distinct demands, so its length is 2j + 1 for j distinct demands, and at most len(demands) phases route flow; - targets are normalised with Fraction(int(n), int(d)). At 84c2082b9 a NumPy integer target kept NumPy parts inside the Fraction, and their products overflowed or wrapped. Production passes float(value), so only direct calls reached this. When the flow falls short, no supply-to-demand arc is saturated, since each can carry the whole requirement. The demands the source cannot reach in the final residual graph then maximise (1 - r) * T - N; that set is the same for every maximum flow and is the union of every maximising set, so the reported set does not depend on the algorithm. One-dimensional weights whose length differs from the masks still raise ValueError, now with its own message; 0-D and 2-D weights, which raised TypeError at 84c2082b9, raise that ValueError too. A NaN or infinite weight on an eligible person, or a NaN or infinite target, raises the same exception and message as before. Timings, the r2 reviewer's script (time_hall_worst.py, seed 0, c = 0.066, independent masks), old and new on the hub (18 CPUs, shared, load1 75-93): people x demands (distinct patterns) 84c2082b9 this commit 20,000 x 10 (1,024) 0.64 s 0.016 s 20,000 x 12 (4,066) 16.5 s 0.027 s 4,000 x 24 (4,000) 44.3 s 0.029 s 20,000 x 24 (19,987) killed at 300 s 0.17 s 100,000 x 4 (16), Belgium's shape 0.10 s 0.044 s Five adversarial families (banded, chain, nested, strata, skewed), 8/16/24 demands, seven shares around the threshold: worst 0.038 s at 20,000 people and 0.157 s at 100,000, never more than 2 routing phases, 159 MB peak RSS. In their sandboxes, reviews r3a and r3b measured 2.72 s and 3.05 s for 20,000 x 24; under macOS background QoS the hub takes 2.35 s. Differential and exhaustive oracle (diff_hall_v2.py): against 84c2082b9's hall_diagnostics and an exact brute force over every demand subset, 25,000 small cases and 1,200 medium cases (50-300 people, 6-11 demands) give 0 oracle failures. The only disagreements with 84c2082b9 (1,643) are its own NumPy-integer overflows. F-C, Belgium (b): compose._node read assignment_knob from a node's own v2 receipt contract without validating it, so a document lacking the field raised a bare KeyError. It now runs validate_receipt_contract on a mapping v2 document first and re-raises as ValueError("Node '' receipt_contract: ..."). v1 and valid v2 documents take the same path as before. A json resource selection is still refused unless it is an object (a selected null reads as not activated), as at the base. A literal or already-resolved parameter holding non-object JSON now reaches the kernel's run-time refusal ("must encode a JSON object"), as on the base 4f333f2c2; from 2f220f70c to 97c08e0f3, _node raised AttributeError on it. F-D, Belgium (a): validate_calibration_inputs raised AttributeError when input_domains[entity] was not a mapping. It now reads as no domain and raises the existing "Calibration input {entity}.{name} needs a domain." at the same point. Tests: - the Hall property test adds subnormal, smallest-normal, 1e-300, 1/3 and 1e300 capacities, and checks the reported set is the union of every maximising set; - test_hall_distinct_patterns_at_scale[0.066-True, 1.0-False]: 20,000 people, 24 demands, independent masks; - test_hall_targets_of_every_real_type_agree: int, float, Fraction, np.int64, np.int32 and np.float64 targets; - test_hall_rational_targets_are_exact: a non-dyadic boundary target, and capacities 4/3 and 1/5 that need the lcm; - test_hall_refuses_weights_that_do_not_align_with_masks; - review r3a's witnesses for four wrong implementations that passed every earlier Hall test: test_hall_keeps_targets_exact_below_float_resolution (targets 1/s +- 2**-200 share one float), test_hall_reroutes_flow_along_reverse_arcs, and test_hall_sums_weights_across_chunks[1.25-True, 2.0-False] (65,537 people, one weight in each chunk); - test_hall_ignores_weights_of_people_eligible_for_nothing[nan, inf, -inf]; - review r4's witness, test_hall_keeps_pattern_indices_aligned_across_chunks: two patterns across the chunk boundary, so pattern labels that restart at each chunk now fail; - test_v2_activation_lists_non_mapping_entity_domains[3], test_calibration_inputs_refuse_non_mapping_entity_domains[2] and test_v2_assignment_node_validates_its_own_receipt_contract[valid_contract, literal_not_an_object, drop_assignment_knob, unknown_field]. Hub run on this tree, one file per process at -n 0: test_transport_receipt_v2 117, receipt_v2_kernel 14, reference_selection 60, calibration_columns 91, column_kernels 30, takeup_kernels 38, target_kernels 67, composed_graph 172, transport_graph 15, reference_profiles 123, nz_spec_package 1,201, country_spec 136, spec_engine_country_bundles 21, uk_target_references 30 (5 skipped), money_kernels 40, gate_kernels 31, terminal_kernels 18, gate_battery_contract_pins 20, gate_battery_replay 2, nz_joint_geography 22 (11 skipped) and codecs_geography 22 pass. Sixteen mutants each fail a test that guards their line, and each file is restored byte for byte: lcm to max (on a value case as well as the empty one), no int() target normalisation, the reported set flipped, the bottleneck as max, chunk scaling dropped, the eligible-mass complement, the alignment check, targets rounded through float, no reverse-arc updates, only the first chunk, a per-chunk denominator, ineligible people's weights read, the (a) guard, the (b) validation and the (b) isinstance guard. Review r4's pattern-index mutant fails the new witness. Record corrections from Belgium and reviews r2 and r3: 18 of 31 kernel implementation hashes moved at 51080462, not the five named there; nothing pins them. 97c08e0f3's message said only the widened case changes; two more do. A person table without person_id now raises ValueError in place of KeyError, and an explicit tables={"person": None} raises TypeError in place of ValueError; valid tables are accepted and refused as before. This commit moves 2 of the 31 registered kernel implementation hashes: takeup.assign@1 and calibration.prepare@1 (Belgium's kernel_hashes.py, 97c08e0f3 against this tree). Co-Authored-By: Claude Opus 5.5 --- .../microcosm/build/calibration_predicates.py | 6 +- .../src/microcosm/build/transport/compose.py | 13 +- .../build/transport/receipt_contract.py | 185 ++++++++++++------ .../shared/test_transport_receipt_v2.py | 145 +++++++++++++- .../test_transport_reference_selection.py | 55 ++++++ 5 files changed, 337 insertions(+), 67 deletions(-) diff --git a/packages/microcosm-build/src/microcosm/build/calibration_predicates.py b/packages/microcosm-build/src/microcosm/build/calibration_predicates.py index 67bf9bf08b..a1f0d8f4f6 100644 --- a/packages/microcosm-build/src/microcosm/build/calibration_predicates.py +++ b/packages/microcosm-build/src/microcosm/build/calibration_predicates.py @@ -42,7 +42,11 @@ def validate_calibration_inputs( for name in names: if tables is not None and name not in tables[entity]: raise ValueError(f"Calibration preparation needs {entity}.{name}.") - domain = entity_domains.get(name) + domain = ( + entity_domains.get(name) + if isinstance(entity_domains, Mapping) + else None + ) if not isinstance(domain, Mapping): raise ValueError(f"Calibration input {entity}.{name} needs a domain.") if tables is None: diff --git a/packages/microcosm-build/src/microcosm/build/transport/compose.py b/packages/microcosm-build/src/microcosm/build/transport/compose.py index 7794214d10..b8f4983759 100644 --- a/packages/microcosm-build/src/microcosm/build/transport/compose.py +++ b/packages/microcosm-build/src/microcosm/build/transport/compose.py @@ -1735,7 +1735,18 @@ def _node( params["reference_selection"] = canonical_json(selection).decode("utf-8") if kernel == "takeup.assign@1" and "receipt_contract" in params: receipt_contract = json.loads(params["receipt_contract"]) - if receipt_contract.get("contract_version") == 2: + if ( + isinstance(receipt_contract, Mapping) + and receipt_contract.get("contract_version") == 2 + ): + # Activation checks receipt_contract.json, but a node may select + # another document: refuse it as the kernel would, before use. + try: + validate_receipt_contract(receipt_contract) + except (ValueError, TypeError) as error: + raise ValueError( + f"Node {row.get('id')!r} receipt_contract: {error}" + ) from error if _has_resource(spec, "calibration_bindings"): params["calibration_bindings"] = canonical_json( transport_resource(spec, "calibration_bindings") diff --git a/packages/microcosm-build/src/microcosm/build/transport/receipt_contract.py b/packages/microcosm-build/src/microcosm/build/transport/receipt_contract.py index 1b98b9e028..5a18bcf114 100644 --- a/packages/microcosm-build/src/microcosm/build/transport/receipt_contract.py +++ b/packages/microcosm-build/src/microcosm/build/transport/receipt_contract.py @@ -17,6 +17,7 @@ from microcosm.frame.input_closure import UndeterminedAction, resolve_judgments RECEIPT_RELATIVE_TOLERANCE = 1 / 10**12 +_HALL_WEIGHT_CHUNK = 1 << 16 RECEIPT_UNIT_FIELDS = ("entitlement_entity", "payee", "draw_unit", "projection") @@ -256,86 +257,146 @@ def _ordered_receipt_outputs(outputs, groups): def hall_diagnostics(demands, weights): - """Report one maximally deficient demand set using exact rational max-flow. + """Report one maximally deficient demand set using an exact integer max-flow. A demand is (programme, filter, target, eligibility mask). Split people into equal-neighbourhood weighted supplies. Join source to supplies, supplies to - eligible demands, and demands to sink. Convert each float weight, target and - relative tolerance r with Fraction, and scale demand capacities by (1 - r). - For T > N, the relative Hall rule permits a subset iff N >= (1 - r) * T. - Thus max-flow saturates every demand iff all subsets satisfy that rule. - - The demand nodes on the sink side of a minimum cut maximise (1 - r) * T - N. - When infeasible, report this one set with its unscaled target and supply. - Edmonds-Karp uses O(V * E**2) rational operations, never demand subsets. - A flow divided by people's weights gives mutually exclusive receipt - probabilities, and multiplying those probabilities gives a flow, proving - equivalence for expected counts. This is diagnostic only; draws and priority - may still refuse even when the fractional allocation is feasible. + eligible demands, and demands to sink, and scale demand capacities by + (1 - r). For T > N, the relative Hall rule permits a subset iff + N >= (1 - r) * T. Thus max-flow saturates every demand iff all subsets + satisfy that rule. + + Every weight, target and 1 - r is rational, so one common denominator makes + every capacity an exact integer: the weights share one power of two, and a + rational target adds its own denominator. Each pattern's weights are summed + as integers, a chunk of people at a time. + + Dinic's algorithm finds the flow. Each phase sets BFS levels, then routes a + blocking flow along level-increasing arcs with current-arc pointers. A + shortest augmenting path alternates supplies with distinct demands, so at + most len(demands) phases route flow, and nothing enumerates demand subsets. + + When the flow falls short, no supply-to-demand arc is saturated, since each + can carry the whole requirement. The demands the source cannot reach in the + final residual graph then maximise (1 - r) * T - N. That set is the same for + every maximum flow, and it is the union of all maximising sets. Report it + with its unscaled target and supply. A flow divided by people's weights + gives mutually exclusive receipt probabilities, and multiplying those + probabilities gives a flow, proving equivalence for expected counts. This + is diagnostic only; draws and priority may still refuse even when the + fractional allocation is feasible. """ - grouped_weights = {} + denominator, totals = 1, {} if demands: packed = np.packbits( np.column_stack([item[3] for item in demands]), axis=1, bitorder="little" ) - for packed_pattern, weight in zip(packed, weights, strict=True): - pattern = int.from_bytes(packed_pattern.tobytes(), "little") + weights = np.asarray(weights, dtype=np.float64) + if weights.shape != (len(packed),): + raise ValueError("Receipt weights must align with the demand masks.") + rows, inverse = np.unique(packed, axis=0, return_inverse=True) + # People eligible for no demand join no supply, so their weights are + # never read. + active = packed.any(axis=1) + inverse = inverse.reshape(-1)[active] + weights = weights[active] + chunks = range(0, len(weights), _HALL_WEIGHT_CHUNK) + for start in chunks: + chunk = weights[start : start + _HALL_WEIGHT_CHUNK].tolist() + denominator = max( + denominator, max(value.as_integer_ratio()[1] for value in chunk) + ) + sums = [0] * len(rows) + for start in chunks: + stop = start + _HALL_WEIGHT_CHUNK + for index, value in zip( + inverse[start:stop].tolist(), + weights[start:stop].tolist(), + strict=True, + ): + numerator, value_denominator = value.as_integer_ratio() + sums[index] += numerator * (denominator // value_denominator) + for row, total in zip(rows, sums, strict=True): + pattern = int.from_bytes(row.tobytes(), "little") if pattern: - grouped_weights[pattern] = grouped_weights.get( - pattern, Fraction() - ) + Fraction(float(weight)) - supplies = sorted(grouped_weights.items()) + totals[pattern] = total + supplies = sorted(totals.items()) scale = 1 - Fraction(RECEIPT_RELATIVE_TOLERANCE) + # Fraction keeps a NumPy integer's own type, whose products can overflow. targets = [Fraction(item[2]) for item in demands] - required = sum(targets, Fraction()) * scale + targets = [Fraction(int(t.numerator), int(t.denominator)) for t in targets] + needed = [scale * target for target in targets] + common = math.lcm(denominator, *(value.denominator for value in needed)) + demand_capacity = [ + value.numerator * (common // value.denominator) for value in needed + ] + required = sum(demand_capacity) source, supply_start = 0, 1 demand_start = supply_start + len(supplies) sink = demand_start + len(demands) - graph = [[] for _ in range(sink + 1)] - - def edge(left, right, capacity): - graph[left].append([right, capacity, len(graph[right])]) - graph[right].append([left, Fraction(), len(graph[left]) - 1]) - - for index, (pattern, mass) in enumerate(supplies): + arcs = [[] for _ in range(sink + 1)] + ends, capacity = [], [] + + def edge(left, right, amount): + # Arc a's reverse is a ^ 1. + arcs[left].append(len(ends)) + ends.append(right) + capacity.append(amount) + arcs[right].append(len(ends)) + ends.append(left) + capacity.append(0) + + for index, (pattern, total) in enumerate(supplies): supply = supply_start + index - edge(source, supply, mass) + edge(source, supply, total * (common // denominator)) for demand in range(len(demands)): - if pattern & (1 << demand): + if pattern >> demand & 1: edge(supply, demand_start + demand, required) - for index, target in enumerate(targets): - edge(demand_start + index, sink, scale * target) - flow = Fraction() + for index, amount in enumerate(demand_capacity): + edge(demand_start + index, sink, amount) + flow = 0 while True: - parents = {source: None} + level = [-1] * (sink + 1) + level[source] = 0 queue = deque([source]) - while queue and sink not in parents: - left = queue.popleft() - for index, (right, capacity, _) in enumerate(graph[left]): - if capacity > 0 and right not in parents: - parents[right] = (left, index) - queue.append(right) - if sink not in parents: + while queue: + node = queue.popleft() + for arc in arcs[node]: + if capacity[arc] > 0 and level[ends[arc]] < 0: + level[ends[arc]] = level[node] + 1 + queue.append(ends[arc]) + if level[sink] < 0: break - capacity, right = required, sink - while right != source: - left, index = parents[right] - capacity = min(capacity, graph[left][index][1]) - right = left - right = sink - while right != source: - left, index = parents[right] - arc = graph[left][index] - arc[1] -= capacity - graph[right][arc[2]][1] += capacity - right = left - flow += capacity + pointer = [0] * (sink + 1) + while True: + path, node = [], source + while node != sink: + out = arcs[node] + while pointer[node] < len(out) and not ( + capacity[out[pointer[node]]] > 0 + and level[ends[out[pointer[node]]]] == level[node] + 1 + ): + pointer[node] += 1 + if pointer[node] < len(out): + path.append(out[pointer[node]]) + node = ends[path[-1]] + elif path: + node = ends[path.pop() ^ 1] + pointer[node] += 1 + else: + break + if node != sink: + break + pushed = min(capacity[arc] for arc in path) + for arc in path: + capacity[arc] -= pushed + capacity[arc ^ 1] += pushed + flow += pushed violations = [] if flow < required: + # The last BFS did not reach the sink, so level < 0 marks the sink side. indices = [ - index - for index in range(len(demands)) - if demand_start + index not in parents + index for index in range(len(demands)) if level[demand_start + index] < 0 ] subset = [demands[index] for index in indices] subset_pattern = sum(1 << index for index in indices) @@ -347,13 +408,13 @@ def edge(left, right, capacity): "filters": [item[1] for item in subset], "target": float(sum((targets[index] for index in indices), Fraction())), "eligible_mass": float( - sum( - ( - mass - for pattern, mass in supplies + Fraction( + sum( + total + for pattern, total in supplies if pattern & subset_pattern ), - Fraction(), + denominator, ) ), } diff --git a/packages/microcosm-build/tests/engine_free/shared/test_transport_receipt_v2.py b/packages/microcosm-build/tests/engine_free/shared/test_transport_receipt_v2.py index 295da454f7..5160b9e532 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_transport_receipt_v2.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_transport_receipt_v2.py @@ -954,9 +954,13 @@ def test_hall_diagnostic_checks_mixed_unstratified_and_stratified_subsets(): def _hall_cases(draw): count = draw(st.integers(0, 12)) size = draw(st.integers(0, 8)) - # Binary fractions retain exact, varied float capacities without rounding - # away the comparison against the independent rational reference below. - capacity = st.integers(0, 32).map(lambda value: value / 8) + # Binary fractions give varied capacities; the extremes (subnormal, 1/3, + # 1e300) stress the shared integer denominator. The rational reference + # below is exact for every float. + capacity = st.one_of( + st.integers(0, 32).map(lambda value: value / 8), + st.sampled_from([5e-324, 2.2250738585072014e-308, 1e-300, 1 / 3, 1e300]), + ) weights = np.asarray(draw(st.lists(capacity, min_size=count, max_size=count))) demands = [ ( @@ -1004,6 +1008,14 @@ def deficiency(indices): i for i, demand in enumerate(demands) if demand[0] in violation["programs"] ] assert deficiency(indices) == maximum + # The reported set is the union of every maximising set. + maximisers = [ + members + for subset in range(1, 1 << len(demands)) + for members in [[i for i in range(len(demands)) if subset >> i & 1]] + if deficiency(members) == maximum + ] + assert indices == sorted(set().union(*maximisers)) assert violation["target"] == float( sum((Fraction(demands[i][2]) for i in indices), Fraction()) ) @@ -1039,6 +1051,133 @@ def no_subsets(*args, **kwargs): assert result["violations"] == [] +@pytest.mark.parametrize(("share", "feasible"), [(0.066, True), (1.0, False)]) +def test_hall_distinct_patterns_at_scale(share, feasible): + # Independent masks give almost every person a pattern of their own: about + # 20,000 supplies, a shape Edmonds-Karp over Fractions did not finish. + rng = np.random.default_rng(0) + masks = rng.random((20_000, 24)) < 0.5 + weights = rng.uniform(1, 100, 20_000) + demands = [ + (f"p{index}", None, share * float(weights[masks[:, index]].sum()), mask) + for index, mask in enumerate(masks.T) + ] + result = receipt_module.hall_diagnostics(demands, weights) + assert result["feasible"] is feasible + if not feasible: + (violation,) = result["violations"] + assert violation["programs"] == [f"p{index}" for index in range(24)] + + +@pytest.mark.parametrize( + "target", [4, 4.0, Fraction(4), np.int64(4), np.int32(4), np.float64(4)] +) +def test_hall_targets_of_every_real_type_agree(target): + weights = np.array([0.1, 3.3]) + + def diagnose(value): + return receipt_module.hall_diagnostics( + [ + ("alpha", None, value, np.array([True, True])), + ("beta", "adult", 0.5, np.array([True, False])), + ], + weights, + ) + + assert diagnose(target) == diagnose(4) + assert not diagnose(4)["feasible"] + + +def test_hall_rational_targets_are_exact(): + scale = 1 - Fraction(receipt_module.RECEIPT_RELATIVE_TOLERANCE) + # A target whose denominator is not a power of two, at the exact boundary. + target = Fraction(7) / scale + assert target.denominator & (target.denominator - 1) + demands = [("alpha", None, target, np.ones(1, dtype=bool))] + assert receipt_module.hall_diagnostics(demands, np.array([7.0]))["feasible"] + assert not receipt_module.hall_diagnostics( + demands, np.array([np.nextafter(7.0, 0)]) + )["feasible"] + # Capacities of 4/3 and 1/5 need the lcm of 3, 5 and the weights' 4. Alpha + # is short by 1/12, more than the pair's 1/30, so it alone is reported. + demands = [ + ("alpha", None, Fraction(4, 3) / scale, np.array([True, False])), + ("beta", None, Fraction(1, 5) / scale, np.array([False, True])), + ] + result = receipt_module.hall_diagnostics(demands, np.array([1.25, 0.25])) + assert not result["feasible"] + assert result["violations"][0]["programs"] == ["alpha"] + + +def test_hall_refuses_weights_that_do_not_align_with_masks(): + demands = [("alpha", None, 1.0, np.ones(2, dtype=bool))] + with pytest.raises(ValueError, match="align with the demand masks"): + receipt_module.hall_diagnostics(demands, np.ones(3)) + + +def test_hall_keeps_targets_exact_below_float_resolution(): + scale = 1 - Fraction(receipt_module.RECEIPT_RELATIVE_TOLERANCE) + # Both targets round to the same float but sit either side of the boundary. + below = 1 / scale - Fraction(1, 2**200) + above = 1 / scale + Fraction(1, 2**200) + assert float(below) == float(above) + + def feasible(target): + demands = [("alpha", None, target, np.ones(1, dtype=bool))] + return receipt_module.hall_diagnostics(demands, np.ones(1))["feasible"] + + assert feasible(below) + assert not feasible(above) + + +def test_hall_reroutes_flow_along_reverse_arcs(): + # The first blocking flow sends alpha through the first person and strands + # beta; only undoing that flow along a reverse arc finds the allocation. + demands = [ + ("alpha", None, 1.0, np.array([True, True])), + ("beta", None, 1.0, np.array([True, False])), + ("gamma", None, 0.0, np.array([False, True])), + ] + assert receipt_module.hall_diagnostics(demands, np.ones(2))["feasible"] + + +@pytest.mark.parametrize(("target", "feasible"), [(1.25, True), (2.0, False)]) +def test_hall_sums_weights_across_chunks(target, feasible): + people = receipt_module._HALL_WEIGHT_CHUNK + 1 + weights = np.zeros(people) + # One weight in each chunk, each with its own power-of-two denominator. + weights[0], weights[-1] = 0.5, 1.0 + demands = [("alpha", None, target, np.ones(people, dtype=bool))] + result = receipt_module.hall_diagnostics(demands, weights) + assert result["feasible"] is feasible + if not feasible: + assert result["violations"][0]["eligible_mass"] == 1.5 + + +def test_hall_keeps_pattern_indices_aligned_across_chunks(): + people = receipt_module._HALL_WEIGHT_CHUNK + 1 + weights = np.zeros(people) + weights[0], weights[-1] = 1.0, 1.0 + # Only the last person, alone in the second chunk, is eligible for beta. + beta = np.zeros(people, dtype=bool) + beta[-1] = True + demands = [("alpha", None, 1.0, ~beta), ("beta", None, 1.0, beta)] + assert receipt_module.hall_diagnostics(demands, weights)["feasible"] + + +@pytest.mark.parametrize("weight", [math.nan, math.inf, -math.inf]) +def test_hall_ignores_weights_of_people_eligible_for_nothing(weight): + demands = [("alpha", None, 1.0, np.array([False, True]))] + assert receipt_module.hall_diagnostics( + demands, np.array([weight, 1.0]) + ) == receipt_module.hall_diagnostics(demands, np.array([0.0, 1.0])) + # The same weight on an eligible person still refuses. + eligible = [("alpha", None, 1.0, np.array([True, True]))] + error = ValueError if math.isnan(weight) else OverflowError + with pytest.raises(error, match="cannot convert"): + receipt_module.hall_diagnostics(eligible, np.array([weight, 1.0])) + + def test_numerical_check_underflow_refuses_with_audit(): with pytest.raises( ReceiptAssignmentRefused, match="numerical expected-mass check" diff --git a/packages/microcosm-build/tests/engine_free/shared/test_transport_reference_selection.py b/packages/microcosm-build/tests/engine_free/shared/test_transport_reference_selection.py index 110f630a64..6f97aa9c78 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_transport_reference_selection.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_transport_reference_selection.py @@ -790,6 +790,61 @@ def test_v2_activation_refuses_unsupported_calibration_input_domain(): compose.validate_transport_activation(spec) +@pytest.mark.parametrize("domains", [["age"], "age", None]) +def test_v2_activation_lists_non_mapping_entity_domains(domains): + spec, _ = _v2_spec() + spec["resources"]["calibration_bindings"]["input_domains"]["person"] = domains + with pytest.raises(ValueError, match="Calibration input person.age needs a domain"): + compose.validate_transport_activation(spec) + + +@pytest.mark.parametrize("with_tables", [False, True]) +def test_calibration_inputs_refuse_non_mapping_entity_domains(with_tables): + from microcosm.build.calibration_predicates import validate_calibration_inputs + + spec, _ = _v2_spec() + bindings = deepcopy(spec["resources"]["calibration_bindings"]) + bindings["input_domains"]["person"] = ["age"] + tables = {"person": pd.DataFrame({"age": [30]})} if with_tables else None + with pytest.raises(ValueError, match="Calibration input person.age needs a domain"): + validate_calibration_inputs(tables, bindings) + + +@pytest.mark.parametrize( + "change", + [ + "valid_contract", + "literal_not_an_object", + "drop_assignment_knob", + "unknown_field", + ], +) +def test_v2_assignment_node_validates_its_own_receipt_contract(change): + spec, assignment = _v2_spec() + contract = deepcopy(spec["resources"]["receipt_contract"]["receipts"]) + if change == "drop_assignment_knob": + del contract["assignment_knob"] + elif change == "unknown_field": + contract["unexpected"] = True + # The node selects a document that activation does not check. + spec["resources"]["alt_receipt"] = {"receipts": contract} + assignment = deepcopy(assignment) + for name in ("receipt_contract", "receipt_contract_sha256"): + assignment["params"][name]["resource"] = "alt_receipt" + if change == "literal_not_an_object": + # A json resource selection must already be an object, but a literal + # string parameter need not be; the kernel refuses it at run time. + assignment["params"]["receipt_contract"] = '["receives_benefit"]' + if change in ("valid_contract", "literal_not_an_object"): + compose._node(spec, assignment, CONFIG, {}, profile_selection={}) + return + with pytest.raises( + ValueError, + match="Node 'assign' receipt_contract: Receipt v2 contract has missing", + ): + compose._node(spec, assignment, CONFIG, {}, profile_selection={}) + + @pytest.mark.parametrize("knob_scope", ["central", "scenario"]) def test_v2_assignment_node_key_uses_only_assignment_knob(knob_scope): spec, assignment = _v2_spec() From 64daa4f78f51587fa89280a194f8149370eeed5f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 29 Oct 2026 08:18:18 -0400 Subject: [PATCH 20/24] Import the reference-factor fixtures through test_support, as CI does Two tests imported microcosm_build.reference_factors as a top-level module. The hub's local runs put test_support on PYTHONPATH, so they passed there, but CI imports it as test_support.microcosm_build and stopped engine-free at collection once this stack first ran against main. Every other test in the repo uses the test_support prefix. Co-Authored-By: Claude Opus 5.5 --- .../tests/engine_free/shared/test_reference_factors.py | 2 +- .../tests/engine_free/shared/test_reference_profiles.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_reference_factors.py b/packages/microcosm-build/tests/engine_free/shared/test_reference_factors.py index 6e2045bbd8..4db4d79ddc 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_reference_factors.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_reference_factors.py @@ -8,7 +8,7 @@ import pytest from hypothesis import example, given, settings from hypothesis import strategies as st -from microcosm_build.reference_factors import factor_context_fixture, factor_fixture +from test_support.microcosm_build.reference_factors import factor_context_fixture, factor_fixture from microcosm.build.ledger_artifact import LexicalFloat from microcosm.build.reference_factors import ( diff --git a/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py b/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py index 60e2cee84d..37c1507c7e 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py @@ -9,7 +9,7 @@ import pytest from hypothesis import HealthCheck, given, settings from hypothesis import strategies as st -from microcosm_build.reference_factors import factor_context_fixture +from test_support.microcosm_build.reference_factors import factor_context_fixture from microcosm.build.reference_profiles import ( REFERENCE_DOCUMENT_KEYS, From b157bb9f52b9e3f76668d2087dc32374fb357886 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 29 Oct 2026 08:26:49 -0400 Subject: [PATCH 21/24] Sort the test_support imports as ruff does in CI Co-Authored-By: Claude Opus 5.5 --- .../tests/engine_free/shared/test_reference_factors.py | 5 ++++- .../tests/engine_free/shared/test_reference_profiles.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_reference_factors.py b/packages/microcosm-build/tests/engine_free/shared/test_reference_factors.py index 4db4d79ddc..7d97c71a15 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_reference_factors.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_reference_factors.py @@ -8,7 +8,6 @@ import pytest from hypothesis import example, given, settings from hypothesis import strategies as st -from test_support.microcosm_build.reference_factors import factor_context_fixture, factor_fixture from microcosm.build.ledger_artifact import LexicalFloat from microcosm.build.reference_factors import ( @@ -20,6 +19,10 @@ validate_factor_context, ) from microcosm.graph.canonical import canonical_json +from test_support.microcosm_build.reference_factors import ( + factor_context_fixture, + factor_fixture, +) def _fraction(row): diff --git a/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py b/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py index 37c1507c7e..ab44f469b9 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_reference_profiles.py @@ -9,7 +9,6 @@ import pytest from hypothesis import HealthCheck, given, settings from hypothesis import strategies as st -from test_support.microcosm_build.reference_factors import factor_context_fixture from microcosm.build.reference_profiles import ( REFERENCE_DOCUMENT_KEYS, @@ -18,6 +17,7 @@ validate_reference_resolution, ) from microcosm.graph.canonical import canonical_json +from test_support.microcosm_build.reference_factors import factor_context_fixture from test_support.paths import paths_for From f09ad72694036e30853514699813c2f0423dcbfb Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 29 Oct 2026 09:08:21 -0400 Subject: [PATCH 22/24] Resolve dotted citations that name a class attribute `resolve_dotted_symbol` (shared, from #1407) imports the longest module prefix of a dotted name and walks the rest with `getattr`. For `pkg.mod.Class.method`, `import_module` fails one level up: it reports `pkg.mod.Class` as the missing module (`'pkg.mod' is not a package`). The resolver only skipped an import error that named the exact prefix it tried, so it re-raised instead of falling back to `pkg.mod`. This PR's `nz/money_normalization.json` cites `microcosm.frame.adapters.policyengine_us.PolicyEngineUSEngine._write_and_verify`, the first citation in any package that reaches through a class. `test_every_dotted_microcosm_symbol_resolves[nz]` fails on it: in CI on #1397, and locally on this branch with the old check restored. The resolver now also skips an error that names a parent of the prefix, as the NZ package test's own resolver does, and still re-raises a missing dependency of a module that exists. A citation whose only importable prefix is the top-level package still raises ModuleNotFoundError, as the existing `microcosm.no_such_shard...` case requires. A new test resolves an enum member and a method through their classes. Co-Authored-By: Claude Opus 5.5 --- .../shared/test_spec_source_citations.py | 14 ++++++++++++++ .../microcosm_build/spec_source_citations.py | 9 ++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/microcosm-build/tests/engine_free/shared/test_spec_source_citations.py b/packages/microcosm-build/tests/engine_free/shared/test_spec_source_citations.py index 240ad69690..9e1a37565f 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_spec_source_citations.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_spec_source_citations.py @@ -59,6 +59,7 @@ from __future__ import annotations import ast +import importlib import inspect import json import re @@ -271,6 +272,19 @@ def test_an_unresolvable_dotted_symbol_raises( resolve_dotted_symbol(dotted) +@pytest.mark.parametrize( + "dotted", + [ + "microcosm.frame.concepts.TransportRule.CARRY", + "microcosm.frame.adapters.policyengine_us.PolicyEngineUSEngine._write_and_verify", + ], +) +def test_a_class_attribute_resolves_through_its_module(dotted: str) -> None: + module, cls, attribute = dotted.rsplit(".", 2) + expected = getattr(getattr(importlib.import_module(module), cls), attribute) + assert resolve_dotted_symbol(dotted) is expected + + # --------------------------------------------------------------------------- # Validator properties # --------------------------------------------------------------------------- diff --git a/test_support/microcosm_build/spec_source_citations.py b/test_support/microcosm_build/spec_source_citations.py index f4ae81e01e..bda0528c37 100644 --- a/test_support/microcosm_build/spec_source_citations.py +++ b/test_support/microcosm_build/spec_source_citations.py @@ -1309,9 +1309,16 @@ def resolve_dotted_symbol(dotted: str) -> object: try: target: object = importlib.import_module(name) except ModuleNotFoundError as error: - if error.name != name: + # ``pkg.mod.Class.method`` fails one level up: importing it first + # imports ``pkg.mod.Class``, which is not a module. A missing + # dependency of a module that does exist still raises. + if error.name is None or not ( + name == error.name or name.startswith(error.name + ".") + ): raise continue + if cut == 1: + raise ModuleNotFoundError(dotted, name=dotted) for attribute in parts[cut:]: target = getattr(target, attribute) return target From b36c702f0908740af52e492357712610496caa51 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 29 Oct 2026 09:55:51 -0400 Subject: [PATCH 23/24] Exclude ledger_artifact from the UK compile modules, with its reason 1d5fff46 made `ledger_targets` import `LexicalFloat` from `ledger_artifact`, and `ledger_targets` is a UK target compile module. CI's `test_every_direct_import_of_a_listed_module_is_listed_or_excluded` then failed: every direct import of a listed module must be listed or excluded. The import feeds one type check: `_strict_scalar_equal` reads an opt-in lexical float as `float`. Only the transport factor compiler opts in, which `test_lexical_float_opt_in_is_limited_to_factor_consumers` pins; the UK kernels load plain floats, so no UK value is ever a `LexicalFloat`, and the loaded facts are node inputs hashed by content. That matches the existing exclusions for imports that never move a value, and it keeps the UK kernels' hashed module set as it was. Co-Authored-By: Claude Opus 5.5 --- .../microcosm/build/uk_runtime/target_compile_modules.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/microcosm-build/src/microcosm/build/uk_runtime/target_compile_modules.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/target_compile_modules.py index 58ba918311..4ea77cd56c 100644 --- a/packages/microcosm-build/src/microcosm/build/uk_runtime/target_compile_modules.py +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/target_compile_modules.py @@ -69,6 +69,13 @@ "the feed-pin epoch arithmetic chronicle_feed checks; the pinned " "facts themselves are node inputs hashed by content" ), + "microcosm.build.ledger_artifact": ( + "ledger_targets imports LexicalFloat only so its strict scalar match " + "reads an opt-in lexical float as float; only the transport factor " + "compiler opts in (test_lexical_float_opt_in_is_limited_to_factor_" + "consumers), the UK kernels load plain floats, and the loaded facts " + "are node inputs hashed by content" + ), "microcosm.build.gates": ( "imported for the GateResult and TargetCoverageRequirement types only" ), From 7cd1d267e0aa769314f821d8140aeb1d21ff0160 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 29 Oct 2026 10:55:30 -0400 Subject: [PATCH 24/24] List ledger_artifact among the UK target compile modules Both UK target kernels decode their ledger facts inside run, through full_targets -> load_ledger_consumer_artifact -> _load_fact_rows. The decoded floats feed listed compilers, but source keys hash the raw file bytes and source_hash covers only the modules the kernels pass it, so an edit to the decoder left both UK implementation hashes unchanged (the delta review moved a national target by changing only the default decimal parser). List the module instead of excluding it; its one microcosm.build import, chronicle_epoch, stays excluded, and its reason now names ledger_artifact's schema gate and epoch labels too. This adds no cache invalidation of its own: this branch already edits ledger_targets.py and ledger_artifact.py, so it re-keys every UK target node either way. Co-Authored-By: Claude Opus 5.5 --- .../build/uk_runtime/target_compile_modules.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/microcosm-build/src/microcosm/build/uk_runtime/target_compile_modules.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/target_compile_modules.py index 4ea77cd56c..642a1e912a 100644 --- a/packages/microcosm-build/src/microcosm/build/uk_runtime/target_compile_modules.py +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/target_compile_modules.py @@ -23,6 +23,7 @@ UK_TARGET_COMPILE_MODULE_NAMES: tuple[str, ...] = ( "microcosm.build.cross_grain", + "microcosm.build.ledger_artifact", "microcosm.build.ledger_targets", "microcosm.build.target_materialization", "microcosm.build.target_reference_authoring", @@ -66,15 +67,10 @@ "refuses an undeclared condition and never moves a value" ), "microcosm.build.chronicle_epoch": ( - "the feed-pin epoch arithmetic chronicle_feed checks; the pinned " - "facts themselves are node inputs hashed by content" - ), - "microcosm.build.ledger_artifact": ( - "ledger_targets imports LexicalFloat only so its strict scalar match " - "reads an opt-in lexical float as float; only the transport factor " - "compiler opts in (test_lexical_float_opt_in_is_limited_to_factor_" - "consumers), the UK kernels load plain floats, and the loaded facts " - "are node inputs hashed by content" + "the feed-pin epoch arithmetic chronicle_feed checks, and the " + "consumer schema gate and epoch labels ledger_artifact applies; " + "none moves a fact value, and the pinned facts themselves are node " + "inputs hashed by content" ), "microcosm.build.gates": ( "imported for the GateResult and TargetCoverageRequirement types only"