From 9b4329e94f113a1b27ba814e27cfa6a69906c7ed Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 1 Aug 2026 06:56:06 -0400 Subject: [PATCH 01/11] Add series catalog: canonical registry over observation families One row per series family mined from official_observations.jsonl (period tokens stripped to {P}; release vintages and concept spellings never merged mechanically), seeded with docket-only rows for Thesis docket series not yet observed. UUIDs mint once and persist across regeneration, keyed by concept. Regenerate with scripts/build_series_catalog.py; --check verifies currency. Co-Authored-By: Claude Fable 5 --- ledger/series_catalog.json | 3869 +++++++++++++++++++++++++++++++ scripts/build_series_catalog.py | 366 +++ 2 files changed, 4235 insertions(+) create mode 100644 ledger/series_catalog.json create mode 100644 scripts/build_series_catalog.py diff --git a/ledger/series_catalog.json b/ledger/series_catalog.json new file mode 100644 index 0000000..caeea50 --- /dev/null +++ b/ledger/series_catalog.json @@ -0,0 +1,3869 @@ +{ + "comment": "Canonical series catalog. One row per series family; uuid is minted once and never re-minted (regeneration preserves it by concept). Consumers reference series by uuid or concept only. Regenerate with scripts/build_series_catalog.py; verify with --check. Cross-spelling merges are manual curation: keep the surviving row's uuid, move absorbed spellings to aliases.", + "generator_version": 1, + "observations_sha256": "63127ff427a4aa3884f54dd1ee070ab631c15ebb38f23fdc9095a45e9204109f", + "observation_rows": 168, + "series": [ + { + "uuid": "0a67f2eb-4776-416e-a9d6-8790e4d40d3d", + "concept": "abs.building_approvals.total_dwellings_mom.australia", + "family_patterns": [ + "abs.building_approvals.total_dwellings_mom.australia.{P}" + ], + "status": "observed", + "unit": "percent_growth", + "cadence": "month", + "geography": { + "id": "AU", + "level": "country", + "name": "Australia" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "abs" + ], + "aliases": [ + "abs.building_approvals.total_dwellings_mom.australia.may_2026", + "building-approvals-australia release page" + ], + "rid_patterns": [ + "abs.building_approvals.total_dwellings_mom.australia.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "cb0a5069-e734-4351-a07b-b3c1d3b30c6d", + "concept": "abs.cpi.all_groups.yoy", + "family_patterns": [ + "abs.cpi.all_groups.yoy" + ], + "status": "observed", + "unit": "percent", + "cadence": "month", + "geography": { + "id": "AU", + "level": "country", + "name": "Australia" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "abs" + ], + "aliases": [ + "CPI/3.10001.10.50.M" + ], + "rid_patterns": [ + "abs.cpi.all_groups.yoy.{P}.first_print" + ], + "first_observed_period": "2026-06", + "last_observed_period": "2026-06", + "observation_count": 1 + }, + { + "uuid": "b38445cc-367f-47ca-9bb2-393890aa4d14", + "concept": "abs.cpi.all_groups_annual_rate.australia", + "family_patterns": [ + "abs.cpi.all_groups_annual_rate.australia.{P}" + ], + "status": "observed", + "unit": "percent", + "cadence": "month", + "geography": { + "id": "AU", + "level": "country", + "name": "Australia" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "abs" + ], + "aliases": [ + "CPI/3.10001.10.50.M", + "abs.cpi.all_groups_annual_rate.australia.may_2026" + ], + "rid_patterns": [ + "abs.cpi.all_groups_annual_rate.australia.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "11bb2c93-9bf0-47fa-8609-cf88277b1c01", + "concept": "abs.cpi_indicator.allgroups.yoy", + "family_patterns": [ + "abs.cpi_indicator.allgroups.yoy.{P}" + ], + "status": "observed", + "unit": "percent", + "cadence": "month", + "geography": { + "id": "AU", + "level": "country", + "name": "Australia" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "abs" + ], + "aliases": [ + "CPI/3.10001.10.50.M", + "abs.cpi_indicator.allgroups.yoy.2026-05" + ], + "rid_patterns": [ + "abs.cpi_indicator.allgroups.yoy.{P}" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "73365ddc-a6c2-4d27-af69-dcfc5cea5cfd", + "concept": "abs.labour.employment_change.australia", + "family_patterns": [ + "abs.labour.employment_change.australia.{P}" + ], + "status": "observed", + "unit": "thousands", + "cadence": "month", + "geography": { + "id": "AU", + "level": "country", + "name": "Australia" + }, + "entity": { + "name": "person", + "role": "employed" + }, + "sources": [ + "abs" + ], + "aliases": [ + "LF/M3.3.1599.20.AUS.M", + "abs.labour.employment_change.australia.june_2026", + "abs.labour.employment_change.australia.may_2026" + ], + "rid_patterns": [ + "abs.labour.employment_change.australia.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-06", + "observation_count": 2 + }, + { + "uuid": "843e5bab-22f6-44ad-9df2-8f1e43882ce6", + "concept": "abs.labour.unemployment_rate", + "family_patterns": [ + "abs.labour.unemployment_rate" + ], + "status": "docket-only", + "unit": null, + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "07353377-eccd-4023-b847-8866a3ad386a", + "concept": "abs.labour.unemployment_rate.australia", + "family_patterns": [ + "abs.labour.unemployment_rate.australia.{P}" + ], + "status": "observed", + "unit": "percent", + "cadence": "month", + "geography": { + "id": "AU", + "level": "country", + "name": "Australia" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "abs" + ], + "aliases": [ + "LF/M13.3.1599.20.AUS.M", + "abs.labour.unemployment_rate.australia.june_2026", + "abs.labour.unemployment_rate.australia.may_2026" + ], + "rid_patterns": [ + "abs.labour.unemployment_rate.australia.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-06", + "observation_count": 2 + }, + { + "uuid": "ae863b3f-47fd-4585-b5ea-6beb8e6ceb60", + "concept": "bank_of_canada.overnight_rate.after_june_2026", + "family_patterns": [ + "bank_of_canada.overnight_rate.after_june_2026" + ], + "status": "observed", + "unit": "percent", + "cadence": "month", + "geography": { + "id": "CA", + "level": "country", + "name": "Canada" + }, + "entity": { + "name": "government", + "role": "overnight_target" + }, + "sources": [ + "bank_of_canada" + ], + "aliases": [], + "rid_patterns": [ + "bank_of_canada.overnight_rate.after_june_2026" + ], + "first_observed_period": "2026-06", + "last_observed_period": "2026-06", + "observation_count": 1 + }, + { + "uuid": "019a338e-df79-4c56-af6a-8ea5c610ab9d", + "concept": "bea.core_pce.mom", + "family_patterns": [ + "bea.core_pce.mom" + ], + "status": "docket-only", + "unit": null, + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "f5b03100-26d3-47a2-82f5-ad833e4097de", + "concept": "bea.disposable_personal_income.level", + "family_patterns": [ + "bea.disposable_personal_income.level.{P}" + ], + "status": "observed", + "unit": "usd_billions", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "bea" + ], + "aliases": [ + "DSPI", + "bea.disposable_personal_income.level.may_2026" + ], + "rid_patterns": [ + "bea.disposable_personal_income.level.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "5d4a2990-6fb8-4517-b4d1-df90bf97b0e1", + "concept": "bea.government_social_benefits.level", + "family_patterns": [ + "bea.government_social_benefits.level.{P}" + ], + "status": "observed", + "unit": "usd_billions", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "bea" + ], + "aliases": [ + "A063RC1", + "bea.government_social_benefits.level.may_2026" + ], + "rid_patterns": [ + "bea.government_social_benefits.level.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "8d836dd8-5942-4a56-b554-0bb44554a2fc", + "concept": "bea.government_social_benefits.medicaid", + "family_patterns": [ + "bea.government_social_benefits.medicaid.{P}" + ], + "status": "observed", + "unit": "usd_billions", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "bea" + ], + "aliases": [ + "W729RC1", + "bea.government_social_benefits.medicaid.may_2026" + ], + "rid_patterns": [ + "bea.government_social_benefits.medicaid.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "4eb192e1-d663-482d-b960-2c04ab901f07", + "concept": "bea.government_social_benefits.medicare", + "family_patterns": [ + "bea.government_social_benefits.medicare.{P}" + ], + "status": "observed", + "unit": "usd_billions", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "bea" + ], + "aliases": [ + "W824RC1", + "bea.government_social_benefits.medicare.may_2026" + ], + "rid_patterns": [ + "bea.government_social_benefits.medicare.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "107564f7-ee16-463e-b1a7-7b109a2f3763", + "concept": "bea.government_social_benefits.social_security", + "family_patterns": [ + "bea.government_social_benefits.social_security.{P}" + ], + "status": "observed", + "unit": "usd_billions", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "bea" + ], + "aliases": [ + "W823RC1", + "bea.government_social_benefits.social_security.may_2026" + ], + "rid_patterns": [ + "bea.government_social_benefits.social_security.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "0f56dfe3-2cdd-4f81-9264-fefb74f022d0", + "concept": "bea.pce.core_mom", + "family_patterns": [ + "bea.pce.core_mom.{P}" + ], + "status": "observed", + "unit": "percent_growth", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "bea" + ], + "aliases": [ + "PCEPILFE", + "bea.pce.core_mom.may_2026" + ], + "rid_patterns": [ + "bea.pce.core_mom.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "7e8c7edc-0aeb-4863-ada6-c98445fb835f", + "concept": "bea.pce_price_index.monthly_change", + "family_patterns": [ + "bea.pce_price_index.monthly_change.{P}" + ], + "status": "observed", + "unit": "percent_growth", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "bea" + ], + "aliases": [ + "PCEPI", + "bea.pce_price_index.monthly_change.may_2026" + ], + "rid_patterns": [ + "bea.pce_price_index.monthly_change.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "5aaf7212-b599-4d52-b233-06d82bafee3d", + "concept": "bea.personal_current_taxes.level", + "family_patterns": [ + "bea.personal_current_taxes.level.{P}" + ], + "status": "observed", + "unit": "usd_billions", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "bea" + ], + "aliases": [ + "W055RC1", + "bea.personal_current_taxes.level.may_2026" + ], + "rid_patterns": [ + "bea.personal_current_taxes.level.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "a2ab0909-0db8-4ba9-b1d3-1d109b182334", + "concept": "bea.real_gdp.saar.third_estimate", + "family_patterns": [ + "bea.real_gdp.saar.{P}.third_estimate" + ], + "status": "observed", + "unit": "percent_growth", + "cadence": "quarter", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "bea" + ], + "aliases": [ + "A191RL1Q225SBEA", + "bea.real_gdp.saar.q1_2026.third_estimate" + ], + "rid_patterns": [ + "bea.real_gdp.saar.{P}.third_estimate" + ], + "first_observed_period": "2026-01", + "last_observed_period": "2026-01", + "observation_count": 1 + }, + { + "uuid": "e303a245-6b71-466d-a25a-343c2e51d09e", + "concept": "bea.trade.goods_services_deficit", + "family_patterns": [ + "bea.trade.goods_services_deficit" + ], + "status": "docket-only", + "unit": "usd_billions", + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "63dca7c9-7eb5-4a42-9737-90c0a0bdb11d", + "concept": "bea.wages_and_salaries.level", + "family_patterns": [ + "bea.wages_and_salaries.level.{P}" + ], + "status": "observed", + "unit": "usd_billions", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "bea" + ], + "aliases": [ + "A576RC1", + "bea.wages_and_salaries.level.may_2026" + ], + "rid_patterns": [ + "bea.wages_and_salaries.level.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "ccd49c74-dcde-4ab6-bb40-d90a035a3b67", + "concept": "bls.ces.average_hourly_earnings_private_monthly_change", + "family_patterns": [ + "bls.ces.average_hourly_earnings_private_monthly_change" + ], + "status": "observed", + "unit": "percent_growth", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "person", + "role": "private_nonfarm_payroll_employee" + }, + "sources": [ + "bls" + ], + "aliases": [ + "bls.ces.average_hourly_earnings_private" + ], + "rid_patterns": [ + "bls.ces.average_hourly_earnings_private.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "39252487-0710-42df-8cbe-e4ca60ff31c0", + "concept": "bls.ces.nonfarm_payrolls.change", + "family_patterns": [ + "bls.ces.nonfarm_payrolls.change" + ], + "status": "docket-only", + "unit": null, + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "b2d62920-5414-4e83-b6ed-91fa84fda47b", + "concept": "bls.ces.total_nonfarm_payroll_change", + "family_patterns": [ + "bls.ces.total_nonfarm_payroll_change", + "bls.ces.total_nonfarm_payroll_change.{P}" + ], + "status": "observed", + "unit": "thousands", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "person", + "role": "nonfarm_payroll_employee" + }, + "sources": [ + "bls", + "bls_ces" + ], + "aliases": [ + "PAYEMS", + "bls.ces.total_nonfarm_payroll_change.june_2026" + ], + "rid_patterns": [ + "bls.ces.total_nonfarm_payroll_change.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-06", + "observation_count": 2 + }, + { + "uuid": "17d3f396-e7cf-4955-8c06-b7370df4c735", + "concept": "bls.cpi.owners_equivalent_rent_mom", + "family_patterns": [ + "bls.cpi.owners_equivalent_rent_mom" + ], + "status": "docket-only", + "unit": "percent_growth", + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "3417de03-198f-4a6e-a2f8-f59a357c9e4a", + "concept": "bls.cpi.rent_primary_residence_mom", + "family_patterns": [ + "bls.cpi.rent_primary_residence_mom" + ], + "status": "docket-only", + "unit": "percent_growth", + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "05e7a6ae-90c4-40f5-ab38-2426f213499d", + "concept": "bls.cpi.services_less_energy_mom", + "family_patterns": [ + "bls.cpi.services_less_energy_mom" + ], + "status": "docket-only", + "unit": "percent_growth", + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "173a2bb9-97c3-4a00-a4e8-9ccb7288d5dd", + "concept": "bls.cpi.services_less_rent_shelter_mom", + "family_patterns": [ + "bls.cpi.services_less_rent_shelter_mom" + ], + "status": "docket-only", + "unit": "percent_growth", + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "41881396-3fb0-4a1f-bac9-6c8c58f73d20", + "concept": "bls.cpi.shelter_mom", + "family_patterns": [ + "bls.cpi.shelter_mom" + ], + "status": "docket-only", + "unit": "percent_growth", + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "23a58ec2-2d41-4f17-ae3d-8affeda44fc1", + "concept": "bls.cpi.u.core_mom", + "family_patterns": [ + "bls.cpi.u.core_mom.{P}" + ], + "status": "observed", + "unit": "percent_growth", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "household", + "role": "cpi_u_less_food_energy" + }, + "sources": [ + "bls", + "bls_cpi" + ], + "aliases": [ + "CPILFESL", + "bls.cpi.u.core_mom.june_2026", + "bls.cpi.u.core_mom.may_2026" + ], + "rid_patterns": [ + "bls.cpi.u.core_mom.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-06", + "observation_count": 2 + }, + { + "uuid": "e6cf3897-504a-4fd2-b314-24c38e13cafb", + "concept": "bls.cpi.u.headline_mom", + "family_patterns": [ + "bls.cpi.u.headline_mom.{P}" + ], + "status": "observed", + "unit": "percent_growth", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "household", + "role": "cpi_u_all_items" + }, + "sources": [ + "bls", + "bls_cpi" + ], + "aliases": [ + "CPIAUCSL", + "bls.cpi.u.headline_mom.june_2026", + "bls.cpi.u.headline_mom.may_2026" + ], + "rid_patterns": [ + "bls.cpi.u.headline_mom.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-06", + "observation_count": 2 + }, + { + "uuid": "f6ef8b1d-b769-4e23-9fb6-e8c07df16c37", + "concept": "bls.cps.employed_people_by_occupation.business_financial_operations", + "family_patterns": [ + "bls.cps.employed_people_by_occupation.business_financial_operations.{P}" + ], + "status": "observed", + "unit": "thousands", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "bls_cps" + ], + "aliases": [ + "Business and financial operations occupations", + "bls.cps.employed_people_by_occupation.business_financial_operations.june_2026" + ], + "rid_patterns": [ + "bls.cps.employed_people_by_occupation.business_financial_operations.{P}.first_print" + ], + "first_observed_period": "2026-06", + "last_observed_period": "2026-06", + "observation_count": 1 + }, + { + "uuid": "5d93431c-cfdc-45a2-b62e-8bbc9ce70f6d", + "concept": "bls.cps.employed_people_by_occupation.computer_mathematical", + "family_patterns": [ + "bls.cps.employed_people_by_occupation.computer_mathematical.{P}" + ], + "status": "observed", + "unit": "thousands", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "bls_cps" + ], + "aliases": [ + "Computer and mathematical occupations", + "bls.cps.employed_people_by_occupation.computer_mathematical.june_2026" + ], + "rid_patterns": [ + "bls.cps.employed_people_by_occupation.computer_mathematical.{P}.first_print" + ], + "first_observed_period": "2026-06", + "last_observed_period": "2026-06", + "observation_count": 1 + }, + { + "uuid": "27c44d4d-c9da-46b2-9e0c-17ef7397b53e", + "concept": "bls.cps.employed_people_by_occupation.healthcare_support", + "family_patterns": [ + "bls.cps.employed_people_by_occupation.healthcare_support.{P}" + ], + "status": "observed", + "unit": "thousands", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "bls_cps" + ], + "aliases": [ + "Healthcare support occupations", + "bls.cps.employed_people_by_occupation.healthcare_support.june_2026" + ], + "rid_patterns": [ + "bls.cps.employed_people_by_occupation.healthcare_support.{P}.first_print" + ], + "first_observed_period": "2026-06", + "last_observed_period": "2026-06", + "observation_count": 1 + }, + { + "uuid": "cf7aed6d-c97d-45f0-823b-f8adcff63da0", + "concept": "bls.cps.employed_people_by_occupation.office_administrative_support", + "family_patterns": [ + "bls.cps.employed_people_by_occupation.office_administrative_support.{P}" + ], + "status": "observed", + "unit": "thousands", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "bls_cps" + ], + "aliases": [ + "Office and administrative support occupations", + "bls.cps.employed_people_by_occupation.office_administrative_support.june_2026" + ], + "rid_patterns": [ + "bls.cps.employed_people_by_occupation.office_administrative_support.{P}.first_print" + ], + "first_observed_period": "2026-06", + "last_observed_period": "2026-06", + "observation_count": 1 + }, + { + "uuid": "479d1718-942b-49b7-abae-e606cb088000", + "concept": "bls.cps.employed_people_by_occupation.production", + "family_patterns": [ + "bls.cps.employed_people_by_occupation.production.{P}" + ], + "status": "observed", + "unit": "thousands", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "bls_cps" + ], + "aliases": [ + "Production occupations", + "bls.cps.employed_people_by_occupation.production.june_2026" + ], + "rid_patterns": [ + "bls.cps.employed_people_by_occupation.production.{P}.first_print" + ], + "first_observed_period": "2026-06", + "last_observed_period": "2026-06", + "observation_count": 1 + }, + { + "uuid": "5aa9a7fe-e5fb-4e53-9b74-629ea99985f0", + "concept": "bls.cps.employed_people_by_occupation.transportation_material_moving", + "family_patterns": [ + "bls.cps.employed_people_by_occupation.transportation_material_moving.{P}" + ], + "status": "observed", + "unit": "thousands", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "bls_cps" + ], + "aliases": [ + "Transportation and material moving occupations", + "bls.cps.employed_people_by_occupation.transportation_material_moving.june_2026" + ], + "rid_patterns": [ + "bls.cps.employed_people_by_occupation.transportation_material_moving.{P}.first_print" + ], + "first_observed_period": "2026-06", + "last_observed_period": "2026-06", + "observation_count": 1 + }, + { + "uuid": "be1586b2-7c70-46a4-ad99-fc53800a8834", + "concept": "bls.cps.telework_share", + "family_patterns": [ + "bls.cps.telework_share" + ], + "status": "docket-only", + "unit": null, + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "81af2046-ee63-413d-b990-71b8717e2b8c", + "concept": "bls.cps.u6_underemployment_rate", + "family_patterns": [ + "bls.cps.u6_underemployment_rate" + ], + "status": "docket-only", + "unit": "percent", + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "bbf6d6b8-82c8-4ae5-9b07-ca493b7d717c", + "concept": "bls.cps.unemployment_rate", + "family_patterns": [ + "bls.cps.unemployment_rate", + "bls.cps.unemployment_rate.{P}" + ], + "status": "observed", + "unit": "percent", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "person", + "role": "civilian_labor_force" + }, + "sources": [ + "bls", + "bls_cps" + ], + "aliases": [ + "UNRATE", + "bls.cps.unemployment_rate.june_2026" + ], + "rid_patterns": [ + "bls.cps.unemployment_rate.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-06", + "observation_count": 2 + }, + { + "uuid": "3a7ced38-7a0b-476d-a15d-369bab07ae88", + "concept": "bls.eci.private_wages_salaries_qoq", + "family_patterns": [ + "bls.eci.private_wages_salaries_qoq.{P}" + ], + "status": "observed", + "unit": "percent_growth", + "cadence": "quarter", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "bls_eci" + ], + "aliases": [ + "ECIWAG", + "bls.eci.private_wages_salaries_qoq.2026_q2" + ], + "rid_patterns": [ + "bls.eci.private_wages_salaries_qoq.{P}.first_print" + ], + "first_observed_period": "2026-04", + "last_observed_period": "2026-04", + "observation_count": 1 + }, + { + "uuid": "15d5c333-c7aa-4f17-8c2c-75bc7e1a6c25", + "concept": "bls.eci.total_compensation_private_industry_qoq", + "family_patterns": [ + "bls.eci.total_compensation_private_industry_qoq.{P}" + ], + "status": "observed", + "unit": "percent_growth", + "cadence": "quarter", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "bls_eci" + ], + "aliases": [ + "ECICOM", + "bls.eci.total_compensation_private_industry_qoq.2026_q2" + ], + "rid_patterns": [ + "bls.eci.total_compensation_private_industry_qoq.{P}.first_print" + ], + "first_observed_period": "2026-04", + "last_observed_period": "2026-04", + "observation_count": 1 + }, + { + "uuid": "a2243a6f-a8b0-4b78-8671-97dfdc715158", + "concept": "bls.export_prices.all_commodities_mom", + "family_patterns": [ + "bls.export_prices.all_commodities_mom" + ], + "status": "docket-only", + "unit": "percent_growth", + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "7d4a1345-dbbe-486d-9a5d-6ba17e2b38aa", + "concept": "bls.import_price_index.all_imports_mom", + "family_patterns": [ + "bls.import_price_index.all_imports_mom.{P}" + ], + "status": "observed", + "unit": "percent_growth", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "household", + "role": "all_imports" + }, + "sources": [ + "bls", + "bls_import_export_prices" + ], + "aliases": [ + "IR", + "bls.import_price_index.all_imports", + "bls.import_price_index.all_imports_mom.2026-06", + "bls.import_price_index.all_imports_mom.may_2026" + ], + "rid_patterns": [ + "bls.import_price_index.all_imports_mom.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-06", + "observation_count": 2 + }, + { + "uuid": "ea88768a-c4e8-439c-ad74-2f0fffeec569", + "concept": "bls.jolts.hires_rate", + "family_patterns": [ + "bls.jolts.hires_rate" + ], + "status": "docket-only", + "unit": "percent", + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "fd2da098-04e6-41bf-a921-35b8d62b1bd2", + "concept": "bls.jolts.job_openings", + "family_patterns": [ + "bls.jolts.job_openings.{P}" + ], + "status": "observed", + "unit": "millions", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "bls_jolts" + ], + "aliases": [ + "JTSJOL", + "bls.jolts.job_openings.may_2026" + ], + "rid_patterns": [ + "bls.jolts.job_openings.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "da0efb61-b35b-48fb-9304-d91b618f2098", + "concept": "bls.jolts.job_openings_total", + "family_patterns": [ + "bls.jolts.job_openings_total.{P}" + ], + "status": "observed", + "unit": "thousands", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "bls_jolts" + ], + "aliases": [ + "JTSJOL", + "bls.jolts.job_openings_total.may_2026" + ], + "rid_patterns": [ + "bls.jolts.job_openings_total.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "09f175c0-15cc-4300-a675-e2ec9fdeabc0", + "concept": "bls.jolts.quits_rate", + "family_patterns": [ + "bls.jolts.quits_rate" + ], + "status": "docket-only", + "unit": null, + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "6ffad760-05ec-4272-98f0-265c13d63a14", + "concept": "bls.lns11300000", + "family_patterns": [ + "bls.lns11300000" + ], + "status": "docket-only", + "unit": "percent", + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "511937f5-55fc-4518-8e08-76601bdd0f69", + "concept": "bls.ppi.final_demand_monthly_change", + "family_patterns": [ + "bls.ppi.final_demand_monthly_change.{P}" + ], + "status": "observed", + "unit": "percent_growth", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "household", + "role": "ppi_final_demand" + }, + "sources": [ + "bls" + ], + "aliases": [ + "bls.ppi.final_demand_monthly_change.may_2026" + ], + "rid_patterns": [ + "bls.ppi.final_demand_monthly_change.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "58ed86d3-32a8-40ac-b6e9-3bc013810d22", + "concept": "bls.productivity.nonfarm_qoq_prelim", + "family_patterns": [ + "bls.productivity.nonfarm_qoq_prelim" + ], + "status": "docket-only", + "unit": null, + "cadence": "quarter", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "9f801a0e-c87e-4b5f-979f-ca2838afc3d2", + "concept": "bls.productivity.nonfarm_unit_labor_costs_qoq_prelim", + "family_patterns": [ + "bls.productivity.nonfarm_unit_labor_costs_qoq_prelim" + ], + "status": "docket-only", + "unit": "percent_growth", + "cadence": "quarter", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "0b2d9164-ddc3-436b-86ec-c508788622c9", + "concept": "bls.real_earnings.avg_hourly_mom", + "family_patterns": [ + "bls.real_earnings.avg_hourly_mom" + ], + "status": "docket-only", + "unit": null, + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "718d2c3c-710f-4606-a9c4-c33a50270066", + "concept": "boe.bank_rate.2026_06_18", + "family_patterns": [ + "boe.bank_rate.2026_06_18" + ], + "status": "observed", + "unit": "percent", + "cadence": "month", + "geography": { + "id": "GB", + "level": "country", + "name": "United Kingdom" + }, + "entity": { + "name": "government", + "role": "bank_rate" + }, + "sources": [ + "boe" + ], + "aliases": [ + "boe.bank_rate" + ], + "rid_patterns": [ + "boe.bank_rate.{P}" + ], + "first_observed_period": "2026-06", + "last_observed_period": "2026-06", + "observation_count": 1 + }, + { + "uuid": "4daecbe3-afb1-4e97-9368-0e7d049d7cfa", + "concept": "boe.bank_rate.after_mpc_june_2026", + "family_patterns": [ + "boe.bank_rate.after_mpc_june_2026" + ], + "status": "observed", + "unit": "percent", + "cadence": "month", + "geography": { + "id": "GB", + "level": "country", + "name": "United Kingdom" + }, + "entity": { + "name": "government", + "role": "bank_rate" + }, + "sources": [ + "boe" + ], + "aliases": [ + "boe.bank_rate" + ], + "rid_patterns": [ + "boe.bank_rate.after_mpc_june_2026.first_print" + ], + "first_observed_period": "2026-06", + "last_observed_period": "2026-06", + "observation_count": 1 + }, + { + "uuid": "e762229a-d846-4e3e-a5f2-ec4361a675b6", + "concept": "boj.policy_rate_guideline.after_june_2026", + "family_patterns": [ + "boj.policy_rate_guideline.after_june_2026" + ], + "status": "observed", + "unit": "percent", + "cadence": "month", + "geography": { + "id": "JP", + "level": "country", + "name": "Japan" + }, + "entity": { + "name": "government", + "role": "uncollateralized_overnight_call_rate_guideline" + }, + "sources": [ + "boj" + ], + "aliases": [ + "boj.guideline_uncollateralized_overnight_call_rate" + ], + "rid_patterns": [ + "boj.policy_rate_guideline.after_june_2026" + ], + "first_observed_period": "2026-06", + "last_observed_period": "2026-06", + "observation_count": 1 + }, + { + "uuid": "4b79edf3-c0c3-4e69-afb8-dc5ca9ddda28", + "concept": "census.construction_spending.total_mom", + "family_patterns": [ + "census.construction_spending.total_mom" + ], + "status": "docket-only", + "unit": "percent_growth", + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "0fb9aba6-6d7b-4c8e-8c94-9583717b20c0", + "concept": "census.housing.completions_saar", + "family_patterns": [ + "census.housing.completions_saar" + ], + "status": "docket-only", + "unit": "thousands", + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "8a7fe6b2-c0bc-4714-b363-44fb2260b08d", + "concept": "census.housing.permits_saar", + "family_patterns": [ + "census.housing.permits_saar" + ], + "status": "docket-only", + "unit": "thousands", + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "e4766a52-8647-4e82-b9fa-de3cf3e82edb", + "concept": "census.housing_starts.saar", + "family_patterns": [ + "census.housing_starts.saar.{P}" + ], + "status": "observed", + "unit": "millions", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "dwelling", + "role": "housing_start" + }, + "sources": [ + "census", + "census_housing" + ], + "aliases": [ + "HOUST", + "census.housing_starts.saar.2026-06", + "census.housing_starts.saar.may_2026" + ], + "rid_patterns": [ + "census.housing_starts.saar.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-06", + "observation_count": 2 + }, + { + "uuid": "aa40d039-021c-456b-bd74-7cd9cca7f974", + "concept": "census.m3.durable_goods_new_orders_mom", + "family_patterns": [ + "census.m3.durable_goods_new_orders_mom" + ], + "status": "docket-only", + "unit": "percent_growth", + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "23759735-fa48-4726-8f62-2a8e7ffcf818", + "concept": "census.m3.durable_goods_new_orders_mom.2026_06", + "family_patterns": [ + "census.m3.durable_goods_new_orders_mom.2026_06" + ], + "status": "observed", + "unit": "percent_growth", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "census_m3" + ], + "aliases": [ + "DGORDER" + ], + "rid_patterns": [ + "census.m3.durable_goods_new_orders_mom.2026_06.first_print" + ], + "first_observed_period": "2026-06", + "last_observed_period": "2026-06", + "observation_count": 1 + }, + { + "uuid": "cd4bcf51-3810-4c14-a003-63160c6a68ae", + "concept": "census.m3.durable_goods_shipments_mom", + "family_patterns": [ + "census.m3.durable_goods_shipments_mom" + ], + "status": "docket-only", + "unit": "percent_growth", + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "fe9a6f40-a6a8-47a9-837d-f9bf6b9ddf18", + "concept": "census.m3.durable_goods_shipments_mom.2026_06", + "family_patterns": [ + "census.m3.durable_goods_shipments_mom.2026_06" + ], + "status": "observed", + "unit": "percent_growth", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "census_m3" + ], + "aliases": [ + "AMDMVS" + ], + "rid_patterns": [ + "census.m3.durable_goods_shipments_mom.2026_06.first_print" + ], + "first_observed_period": "2026-06", + "last_observed_period": "2026-06", + "observation_count": 1 + }, + { + "uuid": "cfc55af5-9d5e-41df-b743-abcdb6fefed0", + "concept": "census.marts.adv44x72.monthly_change", + "family_patterns": [ + "census.marts.adv44x72.{P}.monthly_change" + ], + "status": "observed", + "unit": "percent_growth", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "institutional_sector", + "role": "retail_and_food_services_sales" + }, + "sources": [ + "census" + ], + "aliases": [ + "census.marts.adv44x72.may_2026.monthly_change", + "census.marts.advance_retail_and_food_services_sales_mom" + ], + "rid_patterns": [ + "census.marts.adv44x72.{P}.monthly_change.advance" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "6f893276-193d-4f05-b97e-06f148c7e8b7", + "concept": "census.mtis.total_business_inventories_level", + "family_patterns": [ + "census.mtis.total_business_inventories_level.{P}" + ], + "status": "observed", + "unit": "usd_billions", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "institutional_sector", + "role": "total_business_inventory" + }, + "sources": [ + "census" + ], + "aliases": [ + "census.mtis.total_business_inventories", + "census.mtis.total_business_inventories_level.april_2026" + ], + "rid_patterns": [ + "census.mtis.total_business_inventories_level.{P}.first_print" + ], + "first_observed_period": "2026-04", + "last_observed_period": "2026-04", + "observation_count": 1 + }, + { + "uuid": "d068627e-4231-4112-99a2-585da493d712", + "concept": "census.new_residential_sales.new_single_family_houses_sold_saar", + "family_patterns": [ + "census.new_residential_sales.new_single_family_houses_sold_saar" + ], + "status": "docket-only", + "unit": "thousands", + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "64eecb42-6eaf-4d73-a0f4-2e780a381f25", + "concept": "cms.care_compare.nursing_home_occupancy_pct", + "family_patterns": [ + "cms.care_compare.nursing_home_occupancy_pct.{P}" + ], + "status": "observed", + "unit": "percent", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "cms_provider_data" + ], + "aliases": [ + "Average Number of Residents per Day / Number of Certified Beds", + "cms.care_compare.nursing_home_occupancy_pct.2026-07" + ], + "rid_patterns": [ + "cms.care_compare.nursing_home_occupancy_pct.{P}.first_print" + ], + "first_observed_period": "2026-07", + "last_observed_period": "2026-07", + "observation_count": 1 + }, + { + "uuid": "06eaae11-5aa5-47e1-8d6d-281fa61014db", + "concept": "cms.medicaid_pi.beneficiaries_disenrolled_procedural", + "family_patterns": [ + "cms.medicaid_pi.beneficiaries_disenrolled_procedural" + ], + "status": "observed", + "unit": "count", + "cadence": "month", + "geography": { + "id": "0400000US06", + "level": "state", + "name": "California" + }, + "entity": { + "name": "person", + "role": "medicaid_beneficiary" + }, + "sources": [ + "cms" + ], + "aliases": [ + "Beneficiaries Disenrolled for Procedural Reasons at Renewal" + ], + "rid_patterns": [ + "cms.medicaid_pi.beneficiaries_disenrolled_procedural.california.feb_2026.original_submission" + ], + "first_observed_period": "2026-02", + "last_observed_period": "2026-02", + "observation_count": 1 + }, + { + "uuid": "3b0fe92c-cc24-4b5c-8254-d8e594a4f0b1", + "concept": "cms.medicaid_pi.beneficiaries_disenrolled_total", + "family_patterns": [ + "cms.medicaid_pi.beneficiaries_disenrolled_total" + ], + "status": "observed", + "unit": "count", + "cadence": "month", + "geography": { + "id": "0400000US06", + "level": "state", + "name": "California" + }, + "entity": { + "name": "person", + "role": "medicaid_beneficiary" + }, + "sources": [ + "cms" + ], + "aliases": [ + "Beneficiaries Disenrolled at Renewal (Total)" + ], + "rid_patterns": [ + "cms.medicaid_pi.beneficiaries_disenrolled_total.california.feb_2026.original_submission" + ], + "first_observed_period": "2026-02", + "last_observed_period": "2026-02", + "observation_count": 1 + }, + { + "uuid": "51cd0196-3b55-4fd3-a0f6-3b8df633ca5c", + "concept": "cms.medicaid_pi.beneficiaries_renewed_ex_parte", + "family_patterns": [ + "cms.medicaid_pi.beneficiaries_renewed_ex_parte" + ], + "status": "observed", + "unit": "count", + "cadence": "month", + "geography": { + "id": "0400000US06", + "level": "state", + "name": "California" + }, + "entity": { + "name": "person", + "role": "medicaid_beneficiary" + }, + "sources": [ + "cms" + ], + "aliases": [ + "Beneficiaries Whose Coverage Was Renewed on an Ex Parte Basis" + ], + "rid_patterns": [ + "cms.medicaid_pi.beneficiaries_renewed_ex_parte.california.feb_2026.original_submission" + ], + "first_observed_period": "2026-02", + "last_observed_period": "2026-02", + "observation_count": 1 + }, + { + "uuid": "b60b0791-ac85-48e7-9823-f50374906eff", + "concept": "cms.medicaid_pi.beneficiaries_renewed_total", + "family_patterns": [ + "cms.medicaid_pi.beneficiaries_renewed_total" + ], + "status": "observed", + "unit": "count", + "cadence": "month", + "geography": { + "id": "0400000US06", + "level": "state", + "name": "California" + }, + "entity": { + "name": "person", + "role": "medicaid_beneficiary" + }, + "sources": [ + "cms" + ], + "aliases": [ + "Beneficiaries Whose Coverage Was Renewed (Total)" + ], + "rid_patterns": [ + "cms.medicaid_pi.beneficiaries_renewed_total.california.feb_2026.original_submission" + ], + "first_observed_period": "2026-02", + "last_observed_period": "2026-02", + "observation_count": 1 + }, + { + "uuid": "d5cc5546-e4e9-4a65-a101-4cbbdeb836c6", + "concept": "cms.nursing_home_compare.reported_total_nurse_staffing_hprd_us", + "family_patterns": [ + "cms.nursing_home_compare.reported_total_nurse_staffing_hprd_us.{P}" + ], + "status": "observed", + "unit": "ratio", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "cms_provider_data" + ], + "aliases": [ + "Reported Total Nurse Staffing Hours per Resident per Day", + "cms.nursing_home_compare.reported_total_nurse_staffing_hprd_us.2026-07" + ], + "rid_patterns": [ + "cms.nursing_home_compare.reported_total_nurse_staffing_hprd_us.{P}.first_print" + ], + "first_observed_period": "2026-07", + "last_observed_period": "2026-07", + "observation_count": 1 + }, + { + "uuid": "b19d6746-752e-4f4a-a61b-6692ce739e56", + "concept": "dol.eta.continued_claims.sa", + "family_patterns": [ + "dol.eta.continued_claims.sa" + ], + "status": "observed", + "unit": "millions", + "cadence": "week_ending", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "person", + "role": "ui_claimant" + }, + "sources": [ + "dol_eta" + ], + "aliases": [ + "CCSA" + ], + "rid_patterns": [ + "dol.eta.continued_claims.sa.{P}.first_print" + ], + "first_observed_period": "2026-06-27", + "last_observed_period": "2026-07-18", + "observation_count": 4 + }, + { + "uuid": "6c2ea998-62d0-4940-9c61-3025526f0f01", + "concept": "dol.eta.initial_claims.sa.week_ending_2026_06_06", + "family_patterns": [ + "dol.eta.initial_claims.sa.week_ending_2026_06_06" + ], + "status": "observed", + "unit": "thousands", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "person", + "role": "ui_initial_claimant" + }, + "sources": [ + "dol" + ], + "aliases": [], + "rid_patterns": [ + "dol.eta.initial_claims.sa.week_ending_2026_06_06" + ], + "first_observed_period": "2026-06", + "last_observed_period": "2026-06", + "observation_count": 1 + }, + { + "uuid": "a3b6da69-1b25-436f-b7d8-50402da8acd9", + "concept": "ecb.deposit_facility_rate.after_june_2026", + "family_patterns": [ + "ecb.deposit_facility_rate.after_june_2026" + ], + "status": "observed", + "unit": "percent", + "cadence": "month", + "geography": { + "id": "EA", + "level": "country", + "name": "Euro area" + }, + "entity": { + "name": "government", + "role": "deposit_facility" + }, + "sources": [ + "ecb" + ], + "aliases": [], + "rid_patterns": [ + "ecb.deposit_facility_rate.after_june_2026" + ], + "first_observed_period": "2026-06", + "last_observed_period": "2026-06", + "observation_count": 1 + }, + { + "uuid": "5c60f36b-cff1-48ac-bea6-4d530a3a5b7b", + "concept": "estat.jp.cpi.core_exfreshfood.yoy.2026_05", + "family_patterns": [ + "estat.jp.cpi.core_exfreshfood.yoy.2026_05" + ], + "status": "observed", + "unit": "percent", + "cadence": "month", + "geography": { + "id": "JP", + "level": "country", + "name": "Japan" + }, + "entity": { + "name": "household", + "role": "cpi_less_fresh_food" + }, + "sources": [ + "statjp" + ], + "aliases": [ + "japan.cpi.all_items_less_fresh_food_yoy" + ], + "rid_patterns": [ + "estat.jp.cpi.core_exfreshfood.yoy.{P}" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "665e37f8-73b3-4c18-9b6d-c59287b8516a", + "concept": "eurostat.ea.hicp.flash.yoy", + "family_patterns": [ + "eurostat.ea.hicp.flash.yoy", + "eurostat.ea.hicp.flash.yoy.{P}" + ], + "status": "observed", + "unit": "percent", + "cadence": "month", + "geography": { + "id": "EA21", + "level": "region", + "name": "Euro area" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "eurostat" + ], + "aliases": [ + "eurostat.ea.hicp.flash.yoy.2026-06", + "prc_hicp_minr/M.RCH_A.TOTAL.EA21" + ], + "rid_patterns": [ + "eurostat.ea.hicp.flash.yoy.{P}" + ], + "first_observed_period": "2026-06", + "last_observed_period": "2026-07", + "observation_count": 2 + }, + { + "uuid": "93bcc0bf-88fb-43df-8ec5-e388a2bf41dd", + "concept": "eurostat.hicp.all_items_annual_rate.euro_area", + "family_patterns": [ + "eurostat.hicp.all_items_annual_rate.euro_area.{P}" + ], + "status": "observed", + "unit": "percent", + "cadence": "month", + "geography": { + "id": "EA", + "level": "region", + "name": "Euro area" + }, + "entity": { + "name": "household", + "role": "hicp_all_items" + }, + "sources": [ + "eurostat" + ], + "aliases": [ + "eurostat.hicp.all_items_annual_rate.euro_area.june_2026", + "eurostat.hicp.all_items_annual_rate.euro_area.may_2026", + "prc_hicp_minr/M.RCH_A.TOTAL.EA21" + ], + "rid_patterns": [ + "eurostat.hicp.all_items_annual_rate.euro_area.{P}.final_first_print", + "eurostat.hicp.all_items_annual_rate.euro_area.{P}.flash" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-06", + "observation_count": 2 + }, + { + "uuid": "575f6923-d16a-499a-9cf5-808ff49da510", + "concept": "eurostat.hicp.flash.yoy", + "family_patterns": [ + "eurostat.hicp.flash.yoy" + ], + "status": "docket-only", + "unit": null, + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "a75da4ba-3a7c-4b9e-92ee-9417d2d09947", + "concept": "eurostat.industrial_production.euro_area", + "family_patterns": [ + "eurostat.industrial_production.euro_area.{P}" + ], + "status": "observed", + "unit": "percent_growth", + "cadence": "month", + "geography": { + "id": "EA", + "level": "region", + "name": "Euro area" + }, + "entity": { + "name": "institutional_sector", + "role": "industrial_production" + }, + "sources": [ + "eurostat" + ], + "aliases": [ + "eurostat.industrial_production.euro_area.april_2026" + ], + "rid_patterns": [ + "eurostat.industrial_production.euro_area.{P}.first_print" + ], + "first_observed_period": "2026-04", + "last_observed_period": "2026-04", + "observation_count": 1 + }, + { + "uuid": "c1cd6611-a941-4f6d-99e0-937b0cbe5923", + "concept": "eurostat.retail_trade.volume_mom.euro_area", + "family_patterns": [ + "eurostat.retail_trade.volume_mom.euro_area.{P}" + ], + "status": "observed", + "unit": "percent_growth", + "cadence": "month", + "geography": { + "id": "EA21", + "level": "region", + "name": "Euro area" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "eurostat" + ], + "aliases": [ + "eurostat.retail_trade.volume_mom.euro_area.may_2026", + "products-euro-indicators release page (euro area headline)" + ], + "rid_patterns": [ + "eurostat.retail_trade.volume_mom.euro_area.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "f17d2b21-6462-4ffb-bb0d-0f9d9dfc1417", + "concept": "eurostat.unemployment_rate", + "family_patterns": [ + "eurostat.unemployment_rate" + ], + "status": "docket-only", + "unit": null, + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "b52d84dc-c03a-4560-94ca-518486dbf83b", + "concept": "eurostat.unemployment_rate.belgium", + "family_patterns": [ + "eurostat.unemployment_rate.belgium" + ], + "status": "docket-only", + "unit": null, + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "1950ae76-e1ba-4795-aafb-d8dafeddb9d6", + "concept": "eurostat.unemployment_rate.euro_area", + "family_patterns": [ + "eurostat.unemployment_rate.euro_area.{P}" + ], + "status": "observed", + "unit": "percent", + "cadence": "month", + "geography": { + "id": "EA21", + "level": "region", + "name": "Euro area" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "eurostat" + ], + "aliases": [ + "eurostat.unemployment_rate.euro_area.may_2026", + "une_rt_m/M.SA.TOTAL.PC_ACT.T.EA21" + ], + "rid_patterns": [ + "eurostat.unemployment_rate.euro_area.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "adf2cf96-a222-4605-ad9f-e4d3a3f0a8bd", + "concept": "fed.g17.capacity_utilization.manufacturing", + "family_patterns": [ + "fed.g17.capacity_utilization.manufacturing" + ], + "status": "docket-only", + "unit": "percent", + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "0dde114c-ff49-4eef-a502-3f4f2c41ab22", + "concept": "fed.g17.capacity_utilization.total_industry", + "family_patterns": [ + "fed.g17.capacity_utilization.total_industry.{P}" + ], + "status": "observed", + "unit": "percent", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "institutional_sector", + "role": "total_industry_capacity" + }, + "sources": [ + "fed", + "federal_reserve_g17" + ], + "aliases": [ + "TCU", + "fed.g17.capacity_utilization.total_industry.2026-06", + "fed.g17.capacity_utilization.total_industry.may_2026" + ], + "rid_patterns": [ + "fed.g17.capacity_utilization.total_industry.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-06", + "observation_count": 2 + }, + { + "uuid": "9c1a0167-ae6d-4e37-86b3-b80829dbdd47", + "concept": "fed.g17.industrial_production.total_index_mom", + "family_patterns": [ + "fed.g17.industrial_production.total_index_mom.{P}" + ], + "status": "observed", + "unit": "percent_growth", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "institutional_sector", + "role": "total_industrial_production" + }, + "sources": [ + "fed", + "federal_reserve_g17" + ], + "aliases": [ + "INDPRO", + "fed.g17.industrial_production.total_index_mom.2026-06", + "fed.g17.industrial_production.total_index_mom.may_2026" + ], + "rid_patterns": [ + "fed.g17.industrial_production.total_index_mom.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-06", + "observation_count": 2 + }, + { + "uuid": "baadb403-25a3-4701-a5e1-835b302e62b5", + "concept": "fed.g17.manufacturing_production_mom", + "family_patterns": [ + "fed.g17.manufacturing_production_mom" + ], + "status": "docket-only", + "unit": "percent_growth", + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "377d0771-4f1d-44f7-b4bd-33b73b376fcd", + "concept": "fed.g19.consumer_credit_nonrevolving_annual_rate", + "family_patterns": [ + "fed.g19.consumer_credit_nonrevolving_annual_rate" + ], + "status": "docket-only", + "unit": "percent_growth", + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "51bc97ca-1a1b-4e17-ac31-4fa260131484", + "concept": "fed.g19.consumer_credit_revolving_annual_rate", + "family_patterns": [ + "fed.g19.consumer_credit_revolving_annual_rate" + ], + "status": "docket-only", + "unit": "percent_growth", + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "32760935-b8c0-4442-a7c2-356fd154aa95", + "concept": "fed.g19.consumer_credit_total_annual_rate", + "family_patterns": [ + "fed.g19.consumer_credit_total_annual_rate" + ], + "status": "docket-only", + "unit": "percent_growth", + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "4f83c1d3-3f00-4477-9363-be0b04f2165e", + "concept": "fns.snap.application_processing_timeliness_rate", + "family_patterns": [ + "fns.snap.application_processing_timeliness_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "id": "0400000US06", + "level": "state", + "name": "California" + }, + "entity": { + "name": "household", + "role": "snap_applicant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.application_processing_timeliness.california.{P}.official_release" + ], + "first_observed_period": "2024", + "last_observed_period": "2024", + "observation_count": 1 + }, + { + "uuid": "f0935448-d5c3-403e-802f-bf7c92dd6c60", + "concept": "fns.snap.overpayment_error_rate", + "family_patterns": [ + "fns.snap.overpayment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.overpayment_payment_error_rate.us.{P}.official_release" + ], + "first_observed_period": "2024", + "last_observed_period": "2024", + "observation_count": 1 + }, + { + "uuid": "b89d6078-eb8e-4eb1-92e2-cc436e2093b1", + "concept": "fns.snap.share_jurisdictions_at_or_above_6pct", + "family_patterns": [ + "fns.snap.share_jurisdictions_at_or_above_6pct" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "government", + "role": "snap_administering_jurisdiction" + }, + "sources": [ + "fns" + ], + "aliases": [ + "fns.snap.total_payment_error_rate" + ], + "rid_patterns": [ + "fns.snap.share_jurisdictions_at_or_above_6pct.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "38261880-d350-48fe-ab00-09580ed93dd2", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.ak.{P}", + "fns.snap.total_payment_error_rate.al.{P}", + "fns.snap.total_payment_error_rate.ar.{P}", + "fns.snap.total_payment_error_rate.az.{P}", + "fns.snap.total_payment_error_rate.ca.{P}", + "fns.snap.total_payment_error_rate.co.{P}", + "fns.snap.total_payment_error_rate.ct.{P}", + "fns.snap.total_payment_error_rate.dc.{P}", + "fns.snap.total_payment_error_rate.de.{P}", + "fns.snap.total_payment_error_rate.fl.{P}", + "fns.snap.total_payment_error_rate.ga.{P}", + "fns.snap.total_payment_error_rate.gu.{P}", + "fns.snap.total_payment_error_rate.hi.{P}", + "fns.snap.total_payment_error_rate.ia.{P}", + "fns.snap.total_payment_error_rate.id.{P}", + "fns.snap.total_payment_error_rate.il.{P}", + "fns.snap.total_payment_error_rate.in.{P}", + "fns.snap.total_payment_error_rate.ks.{P}", + "fns.snap.total_payment_error_rate.ky.{P}", + "fns.snap.total_payment_error_rate.la.{P}", + "fns.snap.total_payment_error_rate.ma.{P}", + "fns.snap.total_payment_error_rate.md.{P}", + "fns.snap.total_payment_error_rate.me.{P}", + "fns.snap.total_payment_error_rate.mi.{P}", + "fns.snap.total_payment_error_rate.mn.{P}", + "fns.snap.total_payment_error_rate.mo.{P}", + "fns.snap.total_payment_error_rate.ms.{P}", + "fns.snap.total_payment_error_rate.mt.{P}", + "fns.snap.total_payment_error_rate.nc.{P}", + "fns.snap.total_payment_error_rate.nd.{P}", + "fns.snap.total_payment_error_rate.ne.{P}", + "fns.snap.total_payment_error_rate.nh.{P}", + "fns.snap.total_payment_error_rate.nj.{P}", + "fns.snap.total_payment_error_rate.nm.{P}", + "fns.snap.total_payment_error_rate.nv.{P}", + "fns.snap.total_payment_error_rate.ny.{P}", + "fns.snap.total_payment_error_rate.oh.{P}", + "fns.snap.total_payment_error_rate.ok.{P}", + "fns.snap.total_payment_error_rate.or.{P}", + "fns.snap.total_payment_error_rate.pa.{P}", + "fns.snap.total_payment_error_rate.ri.{P}", + "fns.snap.total_payment_error_rate.sc.{P}", + "fns.snap.total_payment_error_rate.sd.{P}", + "fns.snap.total_payment_error_rate.tn.{P}", + "fns.snap.total_payment_error_rate.tx.{P}", + "fns.snap.total_payment_error_rate.us.{P}", + "fns.snap.total_payment_error_rate.us.{P}.official_release", + "fns.snap.total_payment_error_rate.ut.{P}", + "fns.snap.total_payment_error_rate.va.{P}", + "fns.snap.total_payment_error_rate.vi.{P}", + "fns.snap.total_payment_error_rate.vt.{P}", + "fns.snap.total_payment_error_rate.wa.{P}", + "fns.snap.total_payment_error_rate.wi.{P}", + "fns.snap.total_payment_error_rate.wv.{P}", + "fns.snap.total_payment_error_rate.wy.{P}" + ], + "first_observed_period": "2024", + "last_observed_period": "2025", + "observation_count": 55 + }, + { + "uuid": "2a8cd82e-c6a6-4374-b53f-361c8d912ad5", + "concept": "fns.snap.total_persons", + "family_patterns": [ + "fns.snap.total_persons" + ], + "status": "docket-only", + "unit": "millions", + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "d39d61fc-dfdc-4ac2-825e-fc2fae47206c", + "concept": "fns.snap.underpayment_error_rate", + "family_patterns": [ + "fns.snap.underpayment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.underpayment_payment_error_rate.us.{P}.official_release" + ], + "first_observed_period": "2024", + "last_observed_period": "2024", + "observation_count": 1 + }, + { + "uuid": "230d55c0-c136-4cea-870e-df157e7dd8a5", + "concept": "fns.wic.total_participation", + "family_patterns": [ + "fns.wic.total_participation" + ], + "status": "docket-only", + "unit": "millions", + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "cb766816-11af-41c0-ade0-44b8b80070c3", + "concept": "nbb.business_barometer.overall", + "family_patterns": [ + "nbb.business_barometer.overall" + ], + "status": "docket-only", + "unit": "index_points", + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "cc753c6f-3189-40bc-ac91-a3fc030bca77", + "concept": "nbb.consumer_confidence.indicator", + "family_patterns": [ + "nbb.consumer_confidence.indicator" + ], + "status": "docket-only", + "unit": "index_points", + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "d323b820-4286-4af8-abde-ffc23a6a5cf0", + "concept": "nbb.gdp.flash_qoq", + "family_patterns": [ + "nbb.gdp.flash_qoq" + ], + "status": "docket-only", + "unit": null, + "cadence": "quarter", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "5e675e85-d2c9-4752-ac42-12ffd7f2ab19", + "concept": "ons.cpi.annual_rate", + "family_patterns": [ + "ons.cpi.annual_rate.{P}" + ], + "status": "observed", + "unit": "percent", + "cadence": "month", + "geography": { + "id": "GB", + "level": "country", + "name": "United Kingdom" + }, + "entity": { + "name": "household", + "role": "cpi_all_items" + }, + "sources": [ + "ons" + ], + "aliases": [ + "ons.cpi.annual_rate.may_2026" + ], + "rid_patterns": [ + "ons.cpi.annual_rate.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "03049148-7355-45b8-b84f-ea5e2ed68132", + "concept": "ons.cpih.annual_rate.2026_05", + "family_patterns": [ + "ons.cpih.annual_rate.2026_05" + ], + "status": "observed", + "unit": "percent", + "cadence": "month", + "geography": { + "id": "GB", + "level": "country", + "name": "United Kingdom" + }, + "entity": { + "name": "household", + "role": "cpih_all_items" + }, + "sources": [ + "ons" + ], + "aliases": [ + "ons.cpih.annual_rate" + ], + "rid_patterns": [ + "ons.cpih.annual_rate.{P}" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "f555c66e-3046-4937-868c-b177fb90349c", + "concept": "ons.gdp.monthly_growth", + "family_patterns": [ + "ons.gdp.monthly_growth.{P}" + ], + "status": "observed", + "unit": "percent_growth", + "cadence": "month", + "geography": { + "id": "GB", + "level": "country", + "name": "United Kingdom" + }, + "entity": { + "name": "institutional_sector", + "role": "monthly_gdp" + }, + "sources": [ + "ons" + ], + "aliases": [ + "ons.gdp.monthly_growth.april_2026" + ], + "rid_patterns": [ + "ons.gdp.monthly_growth.{P}.first_print" + ], + "first_observed_period": "2026-04", + "last_observed_period": "2026-04", + "observation_count": 1 + }, + { + "uuid": "a52fff34-0ed5-484f-8f9d-351bf58fdc28", + "concept": "ons.hmrc.paye_payrolled_employees", + "family_patterns": [ + "ons.hmrc.paye_payrolled_employees.{P}" + ], + "status": "observed", + "unit": "millions", + "cadence": "month", + "geography": { + "id": "GB", + "level": "country", + "name": "United Kingdom" + }, + "entity": { + "name": "person", + "role": "paye_payrolled_employee" + }, + "sources": [ + "ons" + ], + "aliases": [ + "ons.hmrc.paye_payrolled_employees.may_2026" + ], + "rid_patterns": [ + "ons.hmrc.paye_payrolled_employees.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "1c721376-4909-4a00-8720-20cfc7a974e0", + "concept": "ons.labour.unemployment_rate.february_to_april_2026", + "family_patterns": [ + "ons.labour.unemployment_rate.february_to_april_2026" + ], + "status": "observed", + "unit": "percent", + "cadence": "month", + "geography": { + "id": "GB", + "level": "country", + "name": "United Kingdom" + }, + "entity": { + "name": "person", + "role": "labour_force" + }, + "sources": [ + "ons" + ], + "aliases": [ + "ons.labour.unemployment_rate" + ], + "rid_patterns": [ + "ons.labour.unemployment_rate.february_to_april_2026.first_print" + ], + "first_observed_period": "2026-04", + "last_observed_period": "2026-04", + "observation_count": 1 + }, + { + "uuid": "5be5655b-d8c5-487b-80fa-56398c1a340b", + "concept": "ons.pusf.j5ii.public_sector_net_borrowing_ex_banks", + "family_patterns": [ + "ons.pusf.j5ii.public_sector_net_borrowing_ex_banks.{P}" + ], + "status": "observed", + "unit": "gbp_billions", + "cadence": "month", + "geography": { + "id": "GB", + "level": "country", + "name": "United Kingdom" + }, + "entity": { + "name": "government", + "role": "public_sector_net_borrowing_ex_banks" + }, + "sources": [ + "ons" + ], + "aliases": [ + "ons.pusf.j5ii", + "ons.pusf.j5ii.public_sector_net_borrowing_ex_banks.may_2026" + ], + "rid_patterns": [ + "ons.pusf.j5ii.public_sector_net_borrowing_ex_banks.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "56f25283-15e0-401e-9702-fab516d563bc", + "concept": "ons.retail_sales.volume_mom", + "family_patterns": [ + "ons.retail_sales.volume_mom.{P}" + ], + "status": "observed", + "unit": "percent_growth", + "cadence": "month", + "geography": { + "id": "GB", + "level": "country", + "name": "Great Britain" + }, + "entity": { + "name": "institutional_sector", + "role": "retail_sales_volume" + }, + "sources": [ + "ons" + ], + "aliases": [ + "ons.retail_sales.volume_mom.may_2026" + ], + "rid_patterns": [ + "ons.retail_sales.volume_mom.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "8ef3fb76-ccf9-44cc-b893-45cc3789a2e5", + "concept": "rba.cash_rate_target.after_june_2026", + "family_patterns": [ + "rba.cash_rate_target.after_june_2026" + ], + "status": "observed", + "unit": "percent", + "cadence": "month", + "geography": { + "id": "AU", + "level": "country", + "name": "Australia" + }, + "entity": { + "name": "government", + "role": "cash_rate_target" + }, + "sources": [ + "rba" + ], + "aliases": [ + "rba.cash_rate_target" + ], + "rid_patterns": [ + "rba.cash_rate_target.after_june_2026" + ], + "first_observed_period": "2026-06", + "last_observed_period": "2026-06", + "observation_count": 1 + }, + { + "uuid": "1a66b6f4-2a13-4e1b-a69c-5720e3e48cd9", + "concept": "ssa.ssi.total_recipients", + "family_patterns": [ + "ssa.ssi.total_recipients" + ], + "status": "docket-only", + "unit": "millions", + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "65865379-9dcd-426f-bd52-537563d618b4", + "concept": "statbel.cpi.headline_yoy", + "family_patterns": [ + "statbel.cpi.headline_yoy" + ], + "status": "docket-only", + "unit": null, + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "ed77763d-9352-4f7c-bc54-2d85f12a436d", + "concept": "statbel.health_index.yoy", + "family_patterns": [ + "statbel.health_index.yoy" + ], + "status": "docket-only", + "unit": null, + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "611888ac-a911-4ba8-963d-5b06f8978970", + "concept": "statcan.36-10-0434-01.all_industries.month_to_month_percent_change", + "family_patterns": [ + "statcan.36-10-0434-01.all_industries.month_to_month_percent_change" + ], + "status": "observed", + "unit": "percent_growth", + "cadence": "month", + "geography": { + "id": "CA", + "level": "country", + "name": "Canada" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "statcan" + ], + "aliases": [ + "v65201210" + ], + "rid_patterns": [ + "statcan.36-10-0434-01.all_industries.month_to_month_percent_change.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "56932066-fd94-4d42-b8e7-06dbdd798031", + "concept": "statcan.building_permits.total_value_mom.canada", + "family_patterns": [ + "statcan.building_permits.total_value_mom.canada.{P}" + ], + "status": "observed", + "unit": "percent_growth", + "cadence": "month", + "geography": { + "id": "CA", + "level": "country", + "name": "Canada" + }, + "entity": { + "name": "dwelling", + "role": "total_value" + }, + "sources": [ + "statcan" + ], + "aliases": [ + "statcan.building_permits.total_value_mom.canada.april_2026" + ], + "rid_patterns": [ + "statcan.building_permits.total_value_mom.canada.{P}.first_print" + ], + "first_observed_period": "2026-04", + "last_observed_period": "2026-04", + "observation_count": 1 + }, + { + "uuid": "79e69ae4-7585-47b5-a119-1bef1fe8fa5a", + "concept": "statcan.cpi.all_items_annual_rate.canada", + "family_patterns": [ + "statcan.cpi.all_items_annual_rate.canada.{P}" + ], + "status": "observed", + "unit": "percent", + "cadence": "month", + "geography": { + "id": "CA", + "level": "country", + "name": "Canada" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "statcan" + ], + "aliases": [ + "statcan.cpi.all_items_annual_rate.canada.may_2026", + "v41690973" + ], + "rid_patterns": [ + "statcan.cpi.all_items_annual_rate.canada.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "9ef07249-8a6a-48c1-9a93-6a0170f4b2d9", + "concept": "statcan.cpi.allitems.yoy", + "family_patterns": [ + "statcan.cpi.allitems.yoy.{P}" + ], + "status": "observed", + "unit": "percent", + "cadence": "month", + "geography": { + "id": "CA", + "level": "country", + "name": "Canada" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "statcan" + ], + "aliases": [ + "statcan.cpi.allitems.yoy.2026-05", + "v41690973" + ], + "rid_patterns": [ + "statcan.cpi.allitems.yoy.{P}" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "bbe90a65-3a7e-4a55-b53f-a4bd026a71bb", + "concept": "statcan.employment_insurance.regular_beneficiaries.canada", + "family_patterns": [ + "statcan.employment_insurance.regular_beneficiaries.canada.{P}" + ], + "status": "observed", + "unit": "thousands", + "cadence": "month", + "geography": { + "id": "CA", + "level": "country", + "name": "Canada" + }, + "entity": { + "name": "person", + "role": "regular_employment_insurance_beneficiary" + }, + "sources": [ + "statcan" + ], + "aliases": [ + "statcan.employment_insurance.regular_beneficiaries", + "statcan.employment_insurance.regular_beneficiaries.canada.april_2026", + "statcan.employment_insurance.regular_beneficiaries.canada.may_2026", + "v64549350" + ], + "rid_patterns": [ + "statcan.employment_insurance.regular_beneficiaries.canada.{P}.first_print" + ], + "first_observed_period": "2026-04", + "last_observed_period": "2026-05", + "observation_count": 2 + }, + { + "uuid": "79d82a2e-0005-413e-a27f-68a892cb9c92", + "concept": "statcan.gdp_by_industry.monthly_growth", + "family_patterns": [ + "statcan.gdp_by_industry.monthly_growth.{P}" + ], + "status": "observed", + "unit": "percent_growth", + "cadence": "month", + "geography": { + "id": "CA", + "level": "country", + "name": "Canada" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "statcan" + ], + "aliases": [ + "statcan.gdp_by_industry.monthly_growth.april_2026", + "v65201210" + ], + "rid_patterns": [ + "statcan.gdp_by_industry.monthly_growth.{P}.first_print" + ], + "first_observed_period": "2026-04", + "last_observed_period": "2026-04", + "observation_count": 1 + }, + { + "uuid": "b54ca57d-6487-4449-8004-7654676c6d8c", + "concept": "statcan.lfs.employment_change", + "family_patterns": [ + "statcan.lfs.employment_change" + ], + "status": "observed", + "unit": "thousands", + "cadence": "month", + "geography": { + "id": "CA", + "level": "country", + "name": "Canada" + }, + "entity": { + "name": "person", + "role": "employed" + }, + "sources": [ + "statcan" + ], + "aliases": [], + "rid_patterns": [ + "statcan.lfs.employment_change.canada.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "b6a06cd2-6961-442e-9549-e418a73ee8fb", + "concept": "statcan.lfs.unemployment_rate", + "family_patterns": [ + "statcan.lfs.unemployment_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "month", + "geography": { + "id": "CA", + "level": "country", + "name": "Canada" + }, + "entity": { + "name": "person", + "role": "labour_force" + }, + "sources": [ + "statcan" + ], + "aliases": [], + "rid_patterns": [ + "statcan.lfs.unemployment_rate.canada.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "1bc3ad4b-a496-49be-a37f-2ecc68aa7f9e", + "concept": "statcan.retail_trade.sales_mom.canada", + "family_patterns": [ + "statcan.retail_trade.sales_mom.canada.{P}" + ], + "status": "observed", + "unit": "percent_growth", + "cadence": "month", + "geography": { + "id": "CA", + "level": "country", + "name": "Canada" + }, + "entity": { + "name": "institutional_sector", + "role": "retail_sales" + }, + "sources": [ + "statcan" + ], + "aliases": [ + "statcan.retail_trade.sales_mom", + "statcan.retail_trade.sales_mom.canada.april_2026" + ], + "rid_patterns": [ + "statcan.retail_trade.sales_mom.canada.{P}.first_print" + ], + "first_observed_period": "2026-04", + "last_observed_period": "2026-04", + "observation_count": 1 + }, + { + "uuid": "500d4013-11d3-4953-a655-f66a0a8e6c2e", + "concept": "statcan.wholesale_trade.sales_mom_exclusions.canada", + "family_patterns": [ + "statcan.wholesale_trade.sales_mom_exclusions.canada.{P}" + ], + "status": "observed", + "unit": "percent_growth", + "cadence": "month", + "geography": { + "id": "CA", + "level": "country", + "name": "Canada" + }, + "entity": { + "name": "institutional_sector", + "role": "wholesale_sales_exclusions" + }, + "sources": [ + "statcan" + ], + "aliases": [ + "statcan.wholesale_trade.sales_mom_exclusions", + "statcan.wholesale_trade.sales_mom_exclusions.canada.april_2026" + ], + "rid_patterns": [ + "statcan.wholesale_trade.sales_mom_exclusions.canada.{P}.first_print" + ], + "first_observed_period": "2026-04", + "last_observed_period": "2026-04", + "observation_count": 1 + }, + { + "uuid": "fe21f760-19bc-442d-8096-9ad6fd519da8", + "concept": "statjp.cpi.all_items_annual_rate.japan", + "family_patterns": [ + "statjp.cpi.all_items_annual_rate.japan.{P}" + ], + "status": "observed", + "unit": "percent", + "cadence": "month", + "geography": { + "id": "JP", + "level": "country", + "name": "Japan" + }, + "entity": { + "name": "household", + "role": "cpi_all_items" + }, + "sources": [ + "statjp" + ], + "aliases": [ + "japan.cpi.all_items_yoy", + "statjp.cpi.all_items_annual_rate.japan.may_2026" + ], + "rid_patterns": [ + "statjp.cpi.all_items_annual_rate.japan.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "82e6ff19-58d9-47de-8c9a-290d216b55d6", + "concept": "statjp.cpi.tokyo_all_items_annual_rate", + "family_patterns": [ + "statjp.cpi.tokyo_all_items_annual_rate.{P}" + ], + "status": "observed", + "unit": "percent", + "cadence": "month", + "geography": { + "id": "JP", + "level": "country", + "name": "Japan" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "stat_jp" + ], + "aliases": [ + "e-Stat statInfId 000040461676 (series 0001, all items)", + "statjp.cpi.tokyo_all_items_annual_rate.june_2026" + ], + "rid_patterns": [ + "statjp.cpi.tokyo_all_items_annual_rate.{P}.preliminary" + ], + "first_observed_period": "2026-06", + "last_observed_period": "2026-06", + "observation_count": 1 + }, + { + "uuid": "3fdadece-0a8d-47ef-82e2-5fdd6c1635a3", + "concept": "statjp.cpi.tokyo_all_items_yoy", + "family_patterns": [ + "statjp.cpi.tokyo_all_items_yoy" + ], + "status": "docket-only", + "unit": null, + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "819db852-efc0-4328-b6f9-921812037422", + "concept": "statjp.household_spending.real_yoy.two_or_more_person_households", + "family_patterns": [ + "statjp.household_spending.real_yoy.two_or_more_person_households.{P}" + ], + "status": "observed", + "unit": "percent_growth", + "cadence": "month", + "geography": { + "id": "JP", + "level": "country", + "name": "Japan" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "stat_jp" + ], + "aliases": [ + "stat.go.jp kakei sokuhou monthly page", + "statjp.household_spending.real_yoy.two_or_more_person_households.may_2026" + ], + "rid_patterns": [ + "statjp.household_spending.real_yoy.two_or_more_person_households.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "872a5b54-68b8-43ac-ae8e-2ed34d1d6248", + "concept": "statjp.lfs.unemployment_rate.japan", + "family_patterns": [ + "statjp.lfs.unemployment_rate.japan.{P}" + ], + "status": "observed", + "unit": "percent", + "cadence": "month", + "geography": { + "id": "JP", + "level": "country", + "name": "Japan" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "stat_jp" + ], + "aliases": [ + "stat.go.jp roudou sokuhou monthly page", + "statjp.lfs.unemployment_rate.japan.may_2026" + ], + "rid_patterns": [ + "statjp.lfs.unemployment_rate.japan.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "889ffc28-5927-45d6-b1bd-042cea0eebd9", + "concept": "treasury.mts.monthly_deficit", + "family_patterns": [ + "treasury.mts.monthly_deficit.{P}" + ], + "status": "observed", + "unit": "usd_billions", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "government", + "role": "federal_budget_balance" + }, + "sources": [ + "treasury" + ], + "aliases": [ + "treasury.mts.monthly_deficit.may_2026" + ], + "rid_patterns": [ + "treasury.mts.monthly_deficit.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "eef582df-ebc4-458d-bcb2-c577eec6f486", + "concept": "us.bea.core_pce.mom_sa", + "family_patterns": [ + "us.bea.core_pce.mom_sa.{P}" + ], + "status": "observed", + "unit": "percent_growth", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "bea" + ], + "aliases": [ + "PCEPILFE", + "us.bea.core_pce.mom_sa.2026-05", + "us.bea.core_pce.mom_sa.2026-06" + ], + "rid_patterns": [ + "us.bea.core_pce.mom_sa.{P}" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-06", + "observation_count": 2 + }, + { + "uuid": "9d57de07-64b7-466b-94db-fb5be3b6ad8b", + "concept": "us.census.housing_starts.total_saar.2026_05", + "family_patterns": [ + "us.census.housing_starts.total_saar.2026_05" + ], + "status": "observed", + "unit": "millions", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "dwelling", + "role": "housing_start" + }, + "sources": [ + "census" + ], + "aliases": [ + "census.housing_starts.saar" + ], + "rid_patterns": [ + "us.census.housing_starts.total_saar.{P}" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "20531c77-574e-4a75-b088-efd30e8348de", + "concept": "us.dol.initial_claims.sa", + "family_patterns": [ + "us.dol.initial_claims.sa" + ], + "status": "observed", + "unit": "thousands", + "cadence": "week_ending", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "person", + "role": "ui_claimant" + }, + "sources": [ + "dol_eta" + ], + "aliases": [ + "ICSA" + ], + "rid_patterns": [ + "us.dol.initial_claims.sa.{P}" + ], + "first_observed_period": "2026-06-20", + "last_observed_period": "2026-07-25", + "observation_count": 5 + }, + { + "uuid": "ac51922e-1cd3-492e-a3bf-f1d9abe3086b", + "concept": "us.dol.initial_claims.sa.week_2026_06_13", + "family_patterns": [ + "us.dol.initial_claims.sa.week_2026_06_13" + ], + "status": "observed", + "unit": "thousands", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "person", + "role": "ui_initial_claimant" + }, + "sources": [ + "dol" + ], + "aliases": [ + "dol.eta.initial_claims.sa" + ], + "rid_patterns": [ + "us.dol.initial_claims.sa.{P}" + ], + "first_observed_period": "2026-06", + "last_observed_period": "2026-06", + "observation_count": 1 + }, + { + "uuid": "4adf85b7-5252-4241-811f-c6f29c8b2521", + "concept": "us.fed.fomc.target_range_upper.2026_06", + "family_patterns": [ + "us.fed.fomc.target_range_upper.2026_06" + ], + "status": "observed", + "unit": "percent", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "government", + "role": "federal_funds_target_range_upper" + }, + "sources": [ + "fed" + ], + "aliases": [ + "fomc.federal_funds_target_range_upper" + ], + "rid_patterns": [ + "us.fed.fomc.target_range_upper.{P}" + ], + "first_observed_period": "2026-06", + "last_observed_period": "2026-06", + "observation_count": 1 + }, + { + "uuid": "4d47758b-a8f8-4dde-a2ef-b27b579c4b00", + "concept": "us.frb.industrial_production.total.mom_sa.2026_05", + "family_patterns": [ + "us.frb.industrial_production.total.mom_sa.2026_05" + ], + "status": "observed", + "unit": "percent_growth", + "cadence": "month", + "geography": { + "id": "0100000US", + "level": "country", + "name": "United States" + }, + "entity": { + "name": "institutional_sector", + "role": "total_industrial_production" + }, + "sources": [ + "fed" + ], + "aliases": [ + "fed.g17.industrial_production.total_index_mom" + ], + "rid_patterns": [ + "us.frb.industrial_production.total.mom_sa.{P}" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "d36768f5-d533-4550-aac7-8fe066495e99", + "concept": "usaspending.dod.new_prime_awards", + "family_patterns": [ + "usaspending.dod.new_prime_awards" + ], + "status": "docket-only", + "unit": "millions", + "cadence": "year", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "66c6b363-bcfb-49c3-b2d7-419114d7808d", + "concept": "usaspending.dod.prime_award_obligations", + "family_patterns": [ + "usaspending.dod.prime_award_obligations" + ], + "status": "docket-only", + "unit": "billions USD", + "cadence": "year", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "ad1f0a62-2372-4426-9eff-8feaaccb2e01", + "concept": "usaspending.dod.prime_award_transactions", + "family_patterns": [ + "usaspending.dod.prime_award_transactions" + ], + "status": "docket-only", + "unit": "millions", + "cadence": "year", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "e12a0711-78b7-4516-929c-1b26888cf4d3", + "concept": "usaspending.dod.prime_contract_obligations", + "family_patterns": [ + "usaspending.dod.prime_contract_obligations" + ], + "status": "docket-only", + "unit": "billions USD", + "cadence": "year", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "4c300bbd-097c-4449-854b-620f110c2e96", + "concept": "usaspending.dod.small_business_contract_obligation_share", + "family_patterns": [ + "usaspending.dod.small_business_contract_obligation_share" + ], + "status": "docket-only", + "unit": "percent", + "cadence": "year", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "4c11c9b0-20ab-4344-96c6-9e89d1b9f340", + "concept": "usaspending.dod.unique_prime_contract_recipients", + "family_patterns": [ + "usaspending.dod.unique_prime_contract_recipients" + ], + "status": "docket-only", + "unit": "thousands", + "cadence": "year", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "4e30f398-c834-456f-846d-f73b155f29bc", + "concept": "usda.fsa.crp.enrolled_acres_total", + "family_patterns": [ + "usda.fsa.crp.enrolled_acres_total" + ], + "status": "docket-only", + "unit": "count", + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + } + ] +} diff --git a/scripts/build_series_catalog.py b/scripts/build_series_catalog.py new file mode 100644 index 0000000..39ad10a --- /dev/null +++ b/scripts/build_series_catalog.py @@ -0,0 +1,366 @@ +#!/usr/bin/env python3 +"""Build ledger/series_catalog.json — the canonical series registry. + +The observation file records facts; this catalog records the SERIES those +facts belong to, one row per family, keyed by a UUID that is minted exactly +once and preserved across regenerations. Consumers (Thesis's docket, bill +mappers, permalink surfaces) refer to series by catalog UUID or canonical +concept and never mint parallel identities. + +Family derivation is deterministic: strip period tokens (and nothing else) +from ``source_record_id`` and ``measure.concept``, replacing each with a +``{P}`` placeholder. Release-vintage segments such as ``first_print`` or +``third_estimate`` are preserved — collapsing across vintages is a curation +judgment, done by hand-merging catalog rows (the surviving row keeps its +UUID; absorbed spellings move into ``aliases``). The same applies to +concept-spelling drift (e.g. ``abs.cpi.all_groups.yoy`` vs +``abs.cpi_indicator.allgroups.yoy``): the generator never merges distinct +spellings mechanically. + +Recognized period tokens (dotted segments): + fy2026 | 2026-05 | may_2026 | q1_2026 | 2026_q1 | week_2026-05-02 + +Usage: + python3 scripts/build_series_catalog.py # regenerate + python3 scripts/build_series_catalog.py --docket PATH # seed docket-only rows + python3 scripts/build_series_catalog.py --check # verify committed file is current + +Families whose post-strip concept is identical (patterns differing only in +``{P}`` placement, e.g. ``bls.cps.unemployment_rate`` observed both bare and +period-suffixed) are the same series and merge into one row listing every +observed pattern. + +Idempotent: same observations + same existing catalog -> byte-identical +output. New series mint fresh UUIDv4s; existing series keep theirs +(looked up by ``concept``, the stable identity key). +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import pathlib +import re +import sys +import uuid +from collections import Counter + +ROOT = pathlib.Path(__file__).resolve().parents[1] +OBSERVATIONS = ROOT / "ledger" / "official_observations.jsonl" +CATALOG = ROOT / "ledger" / "series_catalog.json" + +GENERATOR_VERSION = 1 + +MONTHS = [ + "", + "january", "february", "march", "april", "may", "june", + "july", "august", "september", "october", "november", "december", +] + +# Docket cadence words -> ledger period types. +CADENCE_TO_PERIOD_TYPE = { + "weekly": "week_ending", + "monthly": "month", + "quarterly": "quarter", + "annual": "year", + "fiscal_year": "fiscal_year", +} + +_PERIOD_SEGMENT = re.compile( + r"^(?:" + r"fy\d{4}" # fy2026 + r"|\d{4}-\d{2}(?:-\d{2})?" # 2026-05, 2026-05-02 + r"|(?:%s)_\d{4}" # may_2026 + r"|q[1-4]_\d{4}" # q1_2026 + r"|\d{4}_q[1-4]" # 2026_q1 + r"|week_\d{4}-\d{2}-\d{2}" # week_2026-05-02 + r")$" % "|".join(m for m in MONTHS if m) +) + + +def is_period_segment(segment: str) -> bool: + """Whether one dotted segment is a period token.""" + return bool(_PERIOD_SEGMENT.fullmatch(segment)) + + +def family_pattern(identifier: str) -> str: + """Replace every period-token segment with ``{P}``. + + >>> family_pattern("bls.eci.private_wages_salaries_qoq.2026_q2.first_print") + 'bls.eci.private_wages_salaries_qoq.{P}.first_print' + >>> family_pattern("abs.labour.employment_change.australia.june_2026") + 'abs.labour.employment_change.australia.{P}' + >>> family_pattern("abs.cpi.all_groups.yoy") + 'abs.cpi.all_groups.yoy' + >>> family_pattern("usda.fsa.snap.participation.fy2026.october_2025") + 'usda.fsa.snap.participation.{P}.{P}' + """ + segments = identifier.split(".") + return ".".join("{P}" if is_period_segment(s) else s for s in segments) + + +def concept_for(pattern: str) -> str: + """The human-facing canonical concept: the pattern minus its placeholders. + + >>> concept_for("bls.eci.private_wages_salaries_qoq.{P}.first_print") + 'bls.eci.private_wages_salaries_qoq.first_print' + >>> concept_for("abs.cpi.all_groups.yoy") + 'abs.cpi.all_groups.yoy' + """ + return ".".join(s for s in pattern.split(".") if s != "{P}") + + +def _modal(counter: Counter) -> tuple[object, list]: + """Most common value plus the sorted list of variants (if more than one).""" + if not counter: + return None, [] + ranked = counter.most_common() + modal = ranked[0][0] + variants = sorted(str(v) for v, _ in ranked) + return modal, variants if len(ranked) > 1 else [] + + +def build_families(rows: list[dict]) -> dict[str, dict]: + """Group observation rows into families keyed by family_pattern.""" + families: dict[str, dict] = {} + for row in rows: + rid = row.get("source_record_id") + measure = row.get("measure") or {} + concept_raw = measure.get("concept") + if not isinstance(rid, str) or not isinstance(concept_raw, str): + raise SystemExit( + "observation row missing source_record_id or measure.concept: " + f"{json.dumps(row)[:200]}" + ) + pattern = family_pattern(concept_raw) + fam = families.setdefault( + pattern, + { + "concepts": set(), + "source_concepts": set(), + "rid_patterns": set(), + "units": Counter(), + "period_types": Counter(), + "geographies": Counter(), + "entities": Counter(), + "sources": set(), + "period_values": [], + "count": 0, + }, + ) + fam["concepts"].add(concept_raw) + source_concept = measure.get("source_concept") + if isinstance(source_concept, str): + fam["source_concepts"].add(source_concept) + fam["rid_patterns"].add(family_pattern(rid)) + fam["units"][measure.get("unit")] += 1 + period = row.get("period") or {} + fam["period_types"][period.get("type")] += 1 + geography = row.get("geography") or {} + fam["geographies"][ + json.dumps( + {k: geography.get(k) for k in ("level", "id", "name")}, + sort_keys=True, + ) + ] += 1 + entity = row.get("entity") or {} + fam["entities"][json.dumps(entity, sort_keys=True)] += 1 + source = row.get("source") or {} + if isinstance(source.get("source_name"), str): + fam["sources"].add(source["source_name"]) + if period.get("value") is not None: + fam["period_values"].append(str(period["value"])) + fam["count"] += 1 + return families + + +def load_existing_uuids(path: pathlib.Path) -> dict[str, str]: + """concept -> uuid from the committed catalog, if present.""" + if not path.exists(): + return {} + catalog = json.loads(path.read_text(encoding="utf-8")) + return {row["concept"]: row["uuid"] for row in catalog.get("series", [])} + + +def build_catalog( + observations_path: pathlib.Path, + docket_path: pathlib.Path | None, + existing_uuids: dict[str, str], +) -> dict: + raw = observations_path.read_bytes() + rows = [json.loads(line) for line in raw.decode().splitlines() if line.strip()] + families = build_families(rows) + + # Merge families whose post-strip concept is identical: patterns that + # differ only in {P} placement are one series observed under two period + # formattings, not two series. + by_concept_key: dict[str, dict] = {} + for pattern in sorted(families): + fam = families[pattern] + key = concept_for(pattern) + merged = by_concept_key.setdefault( + key, + { + "patterns": set(), + "concepts": set(), + "source_concepts": set(), + "rid_patterns": set(), + "units": Counter(), + "period_types": Counter(), + "geographies": Counter(), + "entities": Counter(), + "sources": set(), + "period_values": [], + "count": 0, + }, + ) + merged["patterns"].add(pattern) + for field in ("concepts", "source_concepts", "rid_patterns", "sources"): + merged[field] |= fam[field] + for field in ("units", "period_types", "geographies", "entities"): + merged[field] += fam[field] + merged["period_values"] += fam["period_values"] + merged["count"] += fam["count"] + + series: list[dict] = [] + for key in sorted(by_concept_key): + fam = by_concept_key[key] + unit, unit_variants = _modal(fam["units"]) + period_type, period_variants = _modal(fam["period_types"]) + geography_json, _ = _modal(fam["geographies"]) + entity_json, _ = _modal(fam["entities"]) + row = { + "uuid": existing_uuids.get(key) or str(uuid.uuid4()), + "concept": key, + "family_patterns": sorted(fam["patterns"]), + "status": "observed", + "unit": unit, + "cadence": period_type, + "geography": json.loads(geography_json) if geography_json else None, + "entity": json.loads(entity_json) if entity_json else None, + "sources": sorted(fam["sources"]), + "aliases": sorted( + (fam["concepts"] | fam["source_concepts"]) - {key} + ), + "rid_patterns": sorted(fam["rid_patterns"]), + "first_observed_period": min(fam["period_values"], default=None), + "last_observed_period": max(fam["period_values"], default=None), + "observation_count": fam["count"], + } + if unit_variants: + row["unit_variants"] = unit_variants + if period_variants: + row["cadence_variants"] = period_variants + series.append(row) + + if docket_path is not None: + docket = json.loads(docket_path.read_text(encoding="utf-8")) + by_concept = {row["concept"]: row for row in series} + alias_index = { + alias: row for row in series for alias in row["aliases"] + } + for entry in docket["series"]: + concept = entry["series"] + cadence_word = entry.get("cadence") + if cadence_word not in CADENCE_TO_PERIOD_TYPE: + raise SystemExit( + f"docket cadence {cadence_word!r} for {concept} has no " + "period-type mapping; extend CADENCE_TO_PERIOD_TYPE" + ) + target_unit = (entry.get("extras") or {}).get("targetUnit") + hit = by_concept.get(concept) or alias_index.get(concept) + if hit is not None: + if concept != hit["concept"] and concept not in hit["aliases"]: + hit["aliases"] = sorted(hit["aliases"] + [concept]) + if hit["unit"] is None and target_unit is not None: + hit["unit"] = target_unit + continue + row = { + "uuid": existing_uuids.get(concept) or str(uuid.uuid4()), + "concept": concept, + "family_patterns": [family_pattern(concept)], + "status": "docket-only", + "unit": target_unit, + "cadence": CADENCE_TO_PERIOD_TYPE[cadence_word], + "geography": None, + "entity": None, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": None, + "last_observed_period": None, + "observation_count": 0, + } + series.append(row) + by_concept[concept] = row + + series.sort(key=lambda row: row["concept"]) + concepts = [row["concept"] for row in series] + if len(concepts) != len(set(concepts)): + dupes = sorted({c for c in concepts if concepts.count(c) > 1}) + raise SystemExit(f"duplicate concepts in catalog output: {dupes}") + return { + "comment": ( + "Canonical series catalog. One row per series family; uuid is " + "minted once and never re-minted (regeneration preserves it by " + "concept). Consumers reference series by uuid or concept " + "only. Regenerate with scripts/build_series_catalog.py; verify " + "with --check. Cross-spelling merges are manual curation: keep " + "the surviving row's uuid, move absorbed spellings to aliases." + ), + "generator_version": GENERATOR_VERSION, + "observations_sha256": hashlib.sha256(raw).hexdigest(), + "observation_rows": len(rows), + "series": series, + } + + +def render(catalog: dict) -> str: + return json.dumps(catalog, indent=2, ensure_ascii=False) + "\n" + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--observations", type=pathlib.Path, default=OBSERVATIONS) + parser.add_argument("--catalog", type=pathlib.Path, default=CATALOG) + parser.add_argument( + "--docket", + type=pathlib.Path, + default=None, + help="optional Thesis docket_series.json to seed docket-only rows", + ) + parser.add_argument( + "--check", + action="store_true", + help="fail if the committed catalog is not current for these inputs", + ) + args = parser.parse_args(argv) + + existing = load_existing_uuids(args.catalog) + catalog = build_catalog(args.observations, args.docket, existing) + body = render(catalog) + + if args.check: + current = args.catalog.read_text(encoding="utf-8") if args.catalog.exists() else "" + if current != body: + sys.stderr.write( + "series_catalog.json is stale for these inputs; regenerate " + "with scripts/build_series_catalog.py\n" + ) + return 1 + print(f"catalog current: {len(catalog['series'])} series") + return 0 + + args.catalog.write_text(body, encoding="utf-8") + observed = sum(1 for r in catalog["series"] if r["status"] == "observed") + docket_only = sum(1 for r in catalog["series"] if r["status"] == "docket-only") + print( + f"wrote {args.catalog}: {len(catalog['series'])} series " + f"({observed} observed, {docket_only} docket-only)" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 6ab4fbebb07aad9032cadc6afbf501e051e6fc6d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 1 Aug 2026 07:59:38 -0400 Subject: [PATCH 02/11] Series catalog v2: identity keyed by concept+geography+entity Responds to the 2026-08-01 adversarial review (all six findings): - identity key now (concept, geography, entity) per the fact-identity ADR (FNS state error rates split into per-state rows; Eurostat flash/final separate); unit/cadence conflicts are hard errors, never modal picks - token grammar covers every live spelling the review found (underscore dates, abbreviated months, week_ending, month ranges, after_* rate states) plus a semantic pass deriving tokens from each row's own period; surviving year-bearing segments are flagged in suspect_segments (now 0) - UUIDs inherit by identity key, then by unique concept/alias match, so upstream renames and hand merges keep identity; curated aliases persist across regeneration; ambiguous matches and UUID collisions are hard errors; --check validates UUID syntax/version/uniqueness - the docket seed is committed (ledger/seeds/) and digest-bound in the header, so bare --check covers the full input set; docket rows without a declared country carry null geography rather than a fabricated one - regression tests from the review's token table + rename/merge/collision cases; CI runs the seeded --check and the test file Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 7 + ledger/seeds/thesis_docket_series.json | 1457 ++++++++++++ ledger/series_catalog.json | 2923 ++++++++++++++++++++---- scripts/build_series_catalog.py | 572 +++-- tests/test_build_series_catalog.py | 220 ++ 5 files changed, 4477 insertions(+), 702 deletions(-) create mode 100644 ledger/seeds/thesis_docket_series.json create mode 100644 tests/test_build_series_catalog.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 665b004..2ceda54 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,11 @@ jobs: - name: Install dependencies run: uv sync --locked --all-extras + - name: Series catalog current and tested + run: | + uv run --locked python scripts/build_series_catalog.py --check + uv run --locked pytest tests/test_build_series_catalog.py -q + - name: Lint Arch surface run: > uv run ruff check @@ -58,6 +63,8 @@ jobs: micro/us/validation_dashboard.py calibration/constraints.py calibration/targets.py + scripts/build_series_catalog.py + tests/test_build_series_catalog.py tests/test_arch_facts.py tests/test_arch_namespace.py tests/test_arch_normalization.py diff --git a/ledger/seeds/thesis_docket_series.json b/ledger/seeds/thesis_docket_series.json new file mode 100644 index 0000000..08027b1 --- /dev/null +++ b/ledger/seeds/thesis_docket_series.json @@ -0,0 +1,1457 @@ +{ + "comment": "Registry for the auto-roll loop (scripts/roll_docket.py). Recurring series derive their next period from the latest published run; a series without a published run may use one reviewed seedPeriod with an exact official release date. Reviewed annual registered-query snapshots instead carry one explicit period and capture window and are never cadence-stepped. Neither seed path cadence-steps itself. The live catalog slug set is the final duplicate guard.", + "series": [ + { + "series": "us.dol.initial_claims.sa", + "cadence": "weekly", + "slug": "initial-claims-week-{period}", + "extras": { + "valueScale": 0.001, + "targetUnit": "thousands" + } + }, + { + "series": "dol.eta.continued_claims.sa", + "cadence": "weekly", + "slug": "continued-claims-week-{period}", + "extras": { + "valueScale": 0.001, + "targetUnit": "millions" + } + }, + { + "series": "bls.cpi.u.headline_mom", + "cadence": "monthly", + "slug": "us-cpi-u-mom-{month}-{year}" + }, + { + "series": "bls.cpi.u.core_mom", + "cadence": "monthly", + "slug": "us-core-cpi-mom-{month}-{year}" + }, + { + "series": "bls.ces.nonfarm_payrolls.change", + "cadence": "monthly", + "slug": "nonfarm-payrolls-{month}-{year}" + }, + { + "series": "bls.cps.unemployment_rate", + "cadence": "monthly", + "slug": "unemployment-rate-{month}-{year}" + }, + { + "series": "bls.jolts.job_openings", + "cadence": "monthly", + "slug": "jolts-openings-{month}-{year}", + "extras": { + "valueScale": 0.001, + "targetUnit": "millions" + } + }, + { + "series": "bls.jolts.quits_rate", + "cadence": "monthly", + "slug": "jolts-quits-rate-{month}-{year}" + }, + { + "series": "bls.real_earnings.avg_hourly_mom", + "cadence": "monthly", + "slug": "us-real-avg-hourly-earnings-mom-{month}-{year}" + }, + { + "series": "bls.cps.telework_share", + "cadence": "monthly", + "slug": "us-telework-rate-{month}-{year}" + }, + { + "series": "bea.core_pce.mom", + "cadence": "monthly", + "slug": "us-core-pce-mom-{month}-{year}" + }, + { + "series": "treasury.mts.monthly_deficit", + "cadence": "monthly", + "slug": "us-mts-deficit-{month}-{year}" + }, + { + "series": "fns.wic.total_participation", + "cadence": "monthly", + "slug": "wic-participation-{month}-{year}", + "extras": { + "valueScale": 0.001, + "targetUnit": "millions" + } + }, + { + "series": "fns.snap.total_persons", + "cadence": "monthly", + "slug": "snap-participation-{month}-{year}", + "extras": { + "valueScale": 0.001, + "targetUnit": "millions", + "sourceBinding": { + "adapter": "generic-url", + "sourceUrl": "https://www.fns.usda.gov/pd/supplemental-nutrition-assistance-program-snap", + "sourceSeriesId": "fns.snap.total_persons", + "field": "Persons", + "table": "FNS SNAP data tables, national monthly participation (thousands)", + "transform": { + "operation": "multiply", + "factor": 0.001 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "ssa.ssi.total_recipients", + "cadence": "monthly", + "slug": "ssi-recipients-{month}-{year}", + "extras": { + "valueScale": 0.001, + "targetUnit": "millions" + } + }, + { + "series": "eurostat.hicp.flash.yoy", + "cadence": "monthly", + "slug": "euro-flash-hicp-{month}-{year}", + "releaseCalendarUrl": "https://ec.europa.eu/eurostat/news/euro-indicators/release-calendar", + "releaseDates": { + "2026-07": "2026-07-31" + }, + "extras": { + "sourceBinding": { + "adapter": "eurostat-api", + "sourceUrl": "https://ec.europa.eu/eurostat/api/dissemination/sdmx/2.1/data/prc_hicp_minr/M.RCH_A.TOTAL.EA21?format=JSON&lastNObservations=36", + "sourceSeriesId": "prc_hicp_minr/M.RCH_A.TOTAL.EA21", + "field": "prc_hicp_minr/M.RCH_A.TOTAL.EA21", + "table": "HICP (ECOICOP ver.2) monthly rates, prc_hicp_minr (all-items annual rate, euro area)", + "transform": { + "operation": "identity", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "eurostat.unemployment_rate", + "cadence": "monthly", + "slug": "euro-area-unemployment-rate-{month}-{year}", + "extras": { + "sourceBinding": { + "adapter": "generic-url", + "sourceUrl": "https://ec.europa.eu/eurostat/api/dissemination/sdmx/2.1/data/une_rt_m/M.SA.TOTAL.PC_ACT.T.EA21?format=JSON", + "sourceSeriesId": "une_rt_m/M.SA.TOTAL.PC_ACT.T.EA21", + "field": "PC_ACT", + "table": "Unemployment by sex and age (une_rt_m), euro area, SA, total; first print captured between monthly releases", + "transform": { + "operation": "multiply", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "abs.cpi.all_groups.yoy", + "cadence": "monthly", + "slug": "australia-cpi-annual-rate-{month}-{year}", + "releaseCalendarUrl": "https://www.abs.gov.au/statistics/economy/price-indexes-and-inflation/consumer-price-index-australia", + "releaseDates": { + "2026-06": "2026-07-29", + "2026-07": "2026-08-26", + "2026-08": "2026-09-30" + }, + "extras": { + "sourceBinding": { + "adapter": "abs-data-api", + "sourceUrl": "https://data.api.abs.gov.au/rest/data/CPI/3.10001.10.50.M?lastNObservations=30&format=jsondata", + "sourceSeriesId": "CPI/3.10001.10.50.M", + "field": "CPI/3.10001.10.50.M", + "table": "Monthly Consumer Price Index (complete monthly CPI, dataflow CPI: annual change, all groups, original, weighted average of eight capital cities)", + "transform": { + "operation": "identity", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "abs.labour.unemployment_rate", + "cadence": "monthly", + "slug": "australia-unemployment-rate-{month}-{year}", + "releaseCalendarUrl": "https://www.abs.gov.au/statistics/labour/employment-and-unemployment/labour-force-australia", + "releaseDates": { + "2026-06": "2026-07-23", + "2026-07": "2026-08-20", + "2026-08": "2026-09-24" + }, + "extras": { + "sourceBinding": { + "adapter": "abs-data-api", + "sourceUrl": "https://data.api.abs.gov.au/rest/data/LF/M13.3.1599.20.AUS.M?lastNObservations=30&format=jsondata", + "sourceSeriesId": "LF/M13.3.1599.20.AUS.M", + "field": "LF/M13.3.1599.20.AUS.M", + "table": "Labour Force, Australia (dataflow LF: unemployment rate, persons, total age, seasonally adjusted, Australia)", + "transform": { + "operation": "identity", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "statcan.gdp_by_industry.monthly_growth", + "cadence": "monthly", + "slug": "canada-monthly-gdp-growth-{month}-{year}", + "releaseCalendarUrl": "https://www150.statcan.gc.ca/n1/release-diffusion/2026-eng.pdf", + "releaseDates": { + "2026-05": "2026-07-31", + "2026-06": "2026-08-28", + "2026-07": "2026-09-29", + "2026-08": "2026-10-30", + "2026-09": "2026-11-30", + "2026-10": "2026-12-23", + "2026-11": "2027-01-29", + "2026-12": "2027-03-01", + "2027-01": "2027-03-31" + }, + "extras": { + "sourceBinding": { + "adapter": "statcan-wds", + "sourceUrl": "https://www150.statcan.gc.ca/t1/wds/rest/getDataFromVectorsAndLatestNPeriods", + "sourceSeriesId": "v65201210", + "field": "v65201210", + "table": "GDP by industry, Table 36-10-0434-01 (all industries, chained 2017 dollars, SA at annual rates)", + "transform": { + "operation": "percent_change_previous_period", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "statcan.cpi.allitems.yoy", + "cadence": "monthly", + "slug": "canada-cpi-annual-rate-{month}-{year}", + "releaseCalendarUrl": "https://www150.statcan.gc.ca/n1/release-diffusion/2026-eng.pdf", + "releaseDates": { + "2026-06": "2026-07-20", + "2026-07": "2026-08-17", + "2026-08": "2026-09-14", + "2026-09": "2026-10-19", + "2026-10": "2026-11-16", + "2026-11": "2026-12-14", + "2026-12": "2027-01-18", + "2027-01": "2027-02-16", + "2027-02": "2027-03-15" + }, + "extras": { + "sourceBinding": { + "adapter": "statcan-wds", + "sourceUrl": "https://www150.statcan.gc.ca/t1/wds/rest/getDataFromVectorsAndLatestNPeriods", + "sourceSeriesId": "v41690973", + "field": "v41690973", + "table": "Consumer Price Index, Table 18-10-0004-01 (all-items, Canada)", + "transform": { + "operation": "percent_change_year_ago", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "statcan.employment_insurance.regular_beneficiaries", + "cadence": "monthly", + "slug": "canada-ei-regular-beneficiaries-{month}-{year}", + "extras": { + "valueScale": 0.001, + "targetUnit": "thousands", + "sourceBinding": { + "adapter": "generic-url", + "sourceUrl": "https://www150.statcan.gc.ca/t1/wds/rest/getDataFromVectorByReferencePeriodRange?vectorIds=64549350", + "sourceSeriesId": "v64549350", + "field": "v64549350", + "table": "Statistics Canada Table 14-10-0011-01, EI regular beneficiaries, Canada, seasonally adjusted (persons)", + "transform": { + "operation": "multiply", + "factor": 0.001 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "statjp.cpi.tokyo_all_items_yoy", + "cadence": "monthly", + "slug": "japan-tokyo-cpi-annual-rate-{month}-{year}-prelim", + "extras": { + "sourceBinding": { + "adapter": "generic-url", + "sourceUrl": "https://www.stat.go.jp/data/cpi/sokuhou/tsuki/index-t.html", + "sourceSeriesId": "statjp.tokyo_cpi.all_items", + "field": "0001", + "table": "2020-base CPI Tokyo ku-area mid-month preliminary; machine-readable vintage is the release's e-Stat table 1-2 workbook (all items, series code 0001), YoY computed from rounded index", + "transform": { + "operation": "multiply", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "statbel.cpi.headline_yoy", + "cadence": "monthly", + "slug": "belgium-cpi-annual-rate-{month}-{year}", + "extras": { + "country": "BE" + } + }, + { + "series": "statbel.health_index.yoy", + "cadence": "monthly", + "slug": "belgium-health-index-annual-rate-{month}-{year}", + "extras": { + "country": "BE" + } + }, + { + "series": "eurostat.unemployment_rate.belgium", + "cadence": "monthly", + "slug": "belgium-unemployment-rate-{month}-{year}", + "extras": { + "country": "BE" + } + }, + { + "series": "nbb.business_barometer.overall", + "cadence": "monthly", + "slug": "belgium-nbb-business-barometer-{month}-{year}", + "extras": { + "country": "BE", + "targetUnit": "index_points" + } + }, + { + "series": "nbb.consumer_confidence.indicator", + "cadence": "monthly", + "slug": "belgium-consumer-confidence-{month}-{year}", + "extras": { + "country": "BE", + "targetUnit": "index_points" + } + }, + { + "series": "nbb.gdp.flash_qoq", + "cadence": "quarterly", + "slug": "belgium-gdp-flash-q{quarter}-{year}", + "extras": { + "country": "BE" + } + }, + { + "series": "bls.productivity.nonfarm_qoq_prelim", + "cadence": "quarterly", + "slug": "us-nonfarm-productivity-q{quarter}-{year}-prelim" + }, + { + "series": "fed.g17.industrial_production.total_index_mom", + "cadence": "monthly", + "slug": "fed-g17-industrial-production-total-index-mom-{month}-{year}", + "extras": { + "targetUnit": "percent_growth", + "sourceBinding": { + "adapter": "alfred-fred", + "sourceUrl": "https://alfred.stlouisfed.org/graph/alfredgraph.csv?id=INDPRO", + "sourceSeriesId": "INDPRO", + "field": "INDPRO", + "table": "G.17 Industrial Production and Capacity Utilization, monthly seasonally adjusted", + "transform": { + "operation": "multiply", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "fed.g17.manufacturing_production_mom", + "cadence": "monthly", + "slug": "us-manufacturing-production-mom-{month}-{year}", + "seedPeriod": "2026-07", + "releaseCalendarUrl": "https://www.federalreserve.gov/releases/g17/release_dates.htm", + "releaseDates": { + "2026-07": "2026-08-18" + }, + "extras": { + "targetUnit": "percent_growth", + "sourceBinding": { + "adapter": "alfred-fred", + "sourceUrl": "https://alfred.stlouisfed.org/graph/alfredgraph.csv?id=IPMAN", + "sourceSeriesId": "IPMAN", + "field": "IPMAN", + "table": "G.17 Industrial Production and Capacity Utilization, monthly seasonally adjusted", + "transform": { + "operation": "multiply", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "fed.g17.capacity_utilization.total_industry", + "cadence": "monthly", + "slug": "fed-g17-capacity-utilization-total-industry-{month}-{year}", + "extras": { + "targetUnit": "percent", + "sourceBinding": { + "adapter": "alfred-fred", + "sourceUrl": "https://alfred.stlouisfed.org/graph/alfredgraph.csv?id=TCU", + "sourceSeriesId": "TCU", + "field": "TCU", + "table": "G.17 Industrial Production and Capacity Utilization, monthly seasonally adjusted", + "transform": { + "operation": "multiply", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "fed.g17.capacity_utilization.manufacturing", + "cadence": "monthly", + "slug": "us-manufacturing-capacity-utilization-{month}-{year}", + "seedPeriod": "2026-07", + "releaseCalendarUrl": "https://www.federalreserve.gov/releases/g17/release_dates.htm", + "releaseDates": { + "2026-07": "2026-08-18" + }, + "extras": { + "targetUnit": "percent", + "sourceBinding": { + "adapter": "alfred-fred", + "sourceUrl": "https://alfred.stlouisfed.org/graph/alfredgraph.csv?id=MCUMFN", + "sourceSeriesId": "MCUMFN", + "field": "MCUMFN", + "table": "G.17 Industrial Production and Capacity Utilization, monthly seasonally adjusted", + "transform": { + "operation": "multiply", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "census.housing_starts.saar", + "cadence": "monthly", + "slug": "census-housing-starts-saar-{month}-{year}", + "extras": { + "valueScale": 0.001, + "targetUnit": "millions", + "sourceBinding": { + "adapter": "alfred-fred", + "sourceUrl": "https://alfred.stlouisfed.org/graph/alfredgraph.csv?id=HOUST", + "sourceSeriesId": "HOUST", + "field": "HOUST", + "table": "New Residential Construction, seasonally adjusted annual rates", + "transform": { + "operation": "multiply", + "factor": 0.001 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "census.housing.permits_saar", + "cadence": "monthly", + "slug": "us-building-permits-{month}-{year}", + "seedPeriod": "2026-07", + "releaseCalendarUrl": "https://www.census.gov/construction/soc/schedule.html", + "releaseDates": { + "2026-07": "2026-08-18" + }, + "extras": { + "targetUnit": "thousands", + "sourceBinding": { + "adapter": "alfred-fred", + "sourceUrl": "https://alfred.stlouisfed.org/graph/alfredgraph.csv?id=PERMIT", + "sourceSeriesId": "PERMIT", + "field": "PERMIT", + "table": "New Residential Construction, seasonally adjusted annual rates", + "transform": { + "operation": "multiply", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "census.housing.completions_saar", + "cadence": "monthly", + "slug": "us-housing-completions-{month}-{year}", + "seedPeriod": "2026-07", + "releaseCalendarUrl": "https://www.census.gov/construction/soc/schedule.html", + "releaseDates": { + "2026-07": "2026-08-18" + }, + "extras": { + "targetUnit": "thousands", + "sourceBinding": { + "adapter": "alfred-fred", + "sourceUrl": "https://alfred.stlouisfed.org/graph/alfredgraph.csv?id=COMPUTSA", + "sourceSeriesId": "COMPUTSA", + "field": "COMPUTSA", + "table": "New Residential Construction, seasonally adjusted annual rates", + "transform": { + "operation": "multiply", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "census.new_residential_sales.new_single_family_houses_sold_saar", + "cadence": "monthly", + "slug": "us-new-home-sales-saar-{month}-{year}", + "extras": { + "targetUnit": "thousands", + "sourceBinding": { + "adapter": "alfred-fred", + "sourceUrl": "https://alfred.stlouisfed.org/graph/alfredgraph.csv?id=HSN1F", + "sourceSeriesId": "HSN1F", + "field": "HSN1F", + "table": "New Residential Sales, Table 1", + "transform": { + "operation": "multiply", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "census.m3.durable_goods_new_orders_mom", + "cadence": "monthly", + "slug": "us-durable-goods-orders-mom-{month}-{year}", + "seedPeriod": "2026-06", + "releaseCalendarUrl": "https://www.census.gov/manufacturing/m3/release_schedule.html", + "releaseDates": { + "2026-06": "2026-07-27" + }, + "extras": { + "targetUnit": "percent_growth", + "sourceBinding": { + "adapter": "alfred-fred", + "sourceUrl": "https://alfred.stlouisfed.org/graph/alfredgraph.csv?id=DGORDER", + "sourceSeriesId": "DGORDER", + "field": "DGORDER", + "table": "Advance Report on Durable Goods Manufacturers' Shipments, Inventories, and Orders", + "transform": { + "operation": "multiply", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "census.m3.durable_goods_shipments_mom", + "cadence": "monthly", + "slug": "us-durable-goods-shipments-mom-{month}-{year}", + "seedPeriod": "2026-06", + "releaseCalendarUrl": "https://www.census.gov/manufacturing/m3/release_schedule.html", + "releaseDates": { + "2026-06": "2026-07-27" + }, + "extras": { + "targetUnit": "percent_growth", + "sourceBinding": { + "adapter": "alfred-fred", + "sourceUrl": "https://alfred.stlouisfed.org/graph/alfredgraph.csv?id=AMDMVS", + "sourceSeriesId": "AMDMVS", + "field": "AMDMVS", + "table": "Advance Report on Durable Goods Manufacturers' Shipments, Inventories, and Orders", + "transform": { + "operation": "multiply", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "census.construction_spending.total_mom", + "cadence": "monthly", + "slug": "us-construction-spending-mom-{month}-{year}", + "seedPeriod": "2026-06", + "releaseCalendarUrl": "https://www.census.gov/construction/c30/release.html", + "releaseDates": { + "2026-06": "2026-08-03" + }, + "extras": { + "targetUnit": "percent_growth", + "sourceBinding": { + "adapter": "alfred-fred", + "sourceUrl": "https://alfred.stlouisfed.org/graph/alfredgraph.csv?id=TTLCONS", + "sourceSeriesId": "TTLCONS", + "field": "TTLCONS", + "table": "Value of Construction Put in Place Survey", + "transform": { + "operation": "multiply", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "census.mtis.total_business_inventories_level", + "cadence": "monthly", + "slug": "us-total-business-inventories-{month}-{year}", + "extras": { + "valueScale": 0.001, + "targetUnit": "usd_billions", + "sourceBinding": { + "adapter": "alfred-fred", + "sourceUrl": "https://alfred.stlouisfed.org/graph/alfredgraph.csv?id=BUSINV", + "sourceSeriesId": "BUSINV", + "field": "BUSINV", + "table": "Manufacturing and Trade Inventories and Sales", + "transform": { + "operation": "multiply", + "factor": 0.001 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "bea.trade.goods_services_deficit", + "cadence": "monthly", + "slug": "us-goods-services-trade-deficit-{month}-{year}", + "extras": { + "valueScale": -0.001, + "targetUnit": "usd_billions", + "sourceBinding": { + "adapter": "alfred-fred", + "sourceUrl": "https://alfred.stlouisfed.org/graph/alfredgraph.csv?id=BOPGSTB", + "sourceSeriesId": "BOPGSTB", + "field": "BOPGSTB", + "table": "U.S. International Trade in Goods and Services, Exhibit 1", + "transform": { + "operation": "multiply", + "factor": -0.001 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "bls.import_price_index.all_imports_mom", + "cadence": "monthly", + "slug": "bls-import-price-index-all-imports-mom-{month}-{year}", + "extras": { + "targetUnit": "percent_growth", + "sourceBinding": { + "adapter": "alfred-fred", + "sourceUrl": "https://alfred.stlouisfed.org/graph/alfredgraph.csv?id=IR", + "sourceSeriesId": "IR", + "field": "IR", + "table": "U.S. Import Price Indexes, Table 1", + "transform": { + "operation": "multiply", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "bls.export_prices.all_commodities_mom", + "cadence": "monthly", + "slug": "us-export-prices-mom-{month}-{year}", + "seedPeriod": "2026-07", + "releaseCalendarUrl": "https://www.bls.gov/schedule/news_release/ximpim.htm", + "releaseDates": { + "2026-07": "2026-08-18" + }, + "extras": { + "targetUnit": "percent_growth", + "sourceBinding": { + "adapter": "alfred-fred", + "sourceUrl": "https://alfred.stlouisfed.org/graph/alfredgraph.csv?id=IQ", + "sourceSeriesId": "IQ", + "field": "IQ", + "table": "U.S. Export Price Indexes, Table 2", + "transform": { + "operation": "multiply", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "bls.ppi.final_demand_monthly_change", + "cadence": "monthly", + "slug": "bls-ppi-final-demand-monthly-change-{month}-{year}", + "extras": { + "targetUnit": "percent_growth", + "sourceBinding": { + "adapter": "alfred-fred", + "sourceUrl": "https://alfred.stlouisfed.org/graph/alfredgraph.csv?id=PPIFIS", + "sourceSeriesId": "PPIFIS", + "field": "PPIFIS", + "table": "Producer Price Index, final demand, seasonally adjusted", + "transform": { + "operation": "multiply", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "bls.eci.total_compensation_private_industry_qoq", + "cadence": "quarterly", + "slug": "us-employment-cost-index-total-compensation-q{quarter}-{year}", + "seedPeriod": "2026-Q3", + "releaseCalendarUrl": "https://www.bls.gov/schedule/news_release/eci.htm", + "releaseDates": { + "2026-Q2": "2026-07-31", + "2026-Q3": "2026-10-30" + }, + "extras": { + "targetUnit": "percent_growth", + "sourceBinding": { + "adapter": "alfred-fred", + "sourceUrl": "https://alfred.stlouisfed.org/graph/alfredgraph.csv?id=ECICOM", + "sourceSeriesId": "ECICOM", + "field": "ECICOM", + "table": "Employment Cost Index, Table 1", + "transform": { + "operation": "multiply", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "bls.eci.private_wages_salaries_qoq", + "cadence": "quarterly", + "slug": "us-eci-private-wages-salaries-q{quarter}-{year}", + "seedPeriod": "2026-Q3", + "releaseCalendarUrl": "https://www.bls.gov/schedule/news_release/eci.htm", + "releaseDates": { + "2026-Q2": "2026-07-31", + "2026-Q3": "2026-10-30" + }, + "extras": { + "targetUnit": "percent_growth", + "sourceBinding": { + "adapter": "alfred-fred", + "sourceUrl": "https://alfred.stlouisfed.org/graph/alfredgraph.csv?id=ECIWAG", + "sourceSeriesId": "ECIWAG", + "field": "ECIWAG", + "table": "Employment Cost Index, Table 2", + "transform": { + "operation": "multiply", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "bls.productivity.nonfarm_unit_labor_costs_qoq_prelim", + "cadence": "quarterly", + "slug": "us-unit-labor-costs-q{quarter}-{year}-prelim", + "seedPeriod": "2026-Q2", + "releaseCalendarUrl": "https://www.bls.gov/schedule/news_release/prod2.htm", + "releaseDates": { + "2026-Q2": "2026-08-06" + }, + "extras": { + "targetUnit": "percent_growth", + "sourceBinding": { + "adapter": "alfred-fred", + "sourceUrl": "https://alfred.stlouisfed.org/graph/alfredgraph.csv?id=PRS85006112", + "sourceSeriesId": "PRS85006112", + "field": "PRS85006112", + "table": "Productivity and Costs, nonfarm business sector", + "transform": { + "operation": "multiply", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "fed.g19.consumer_credit_total_annual_rate", + "cadence": "monthly", + "slug": "us-consumer-credit-annual-rate-{month}-{year}", + "seedPeriod": "2026-06", + "releaseCalendarUrl": "https://www.federalreserve.gov/newsevents/2026-august.htm", + "releaseDates": { + "2026-06": "2026-08-07" + }, + "extras": { + "targetUnit": "percent_growth", + "sourceBinding": { + "adapter": "alfred-fred", + "sourceUrl": "https://alfred.stlouisfed.org/graph/alfredgraph.csv?id=TOTALSLAR", + "sourceSeriesId": "TOTALSLAR", + "field": "TOTALSLAR", + "table": "G.19 Consumer Credit, outstanding, seasonally adjusted", + "transform": { + "operation": "multiply", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "fed.g19.consumer_credit_revolving_annual_rate", + "cadence": "monthly", + "slug": "us-revolving-consumer-credit-annual-rate-{month}-{year}", + "seedPeriod": "2026-06", + "releaseCalendarUrl": "https://www.federalreserve.gov/newsevents/2026-august.htm", + "releaseDates": { + "2026-06": "2026-08-07" + }, + "extras": { + "targetUnit": "percent_growth", + "sourceBinding": { + "adapter": "alfred-fred", + "sourceUrl": "https://alfred.stlouisfed.org/graph/alfredgraph.csv?id=REVOLSLAR", + "sourceSeriesId": "REVOLSLAR", + "field": "REVOLSLAR", + "table": "G.19 Consumer Credit, outstanding, seasonally adjusted", + "transform": { + "operation": "multiply", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "fed.g19.consumer_credit_nonrevolving_annual_rate", + "cadence": "monthly", + "slug": "us-nonrevolving-consumer-credit-annual-rate-{month}-{year}", + "seedPeriod": "2026-06", + "releaseCalendarUrl": "https://www.federalreserve.gov/newsevents/2026-august.htm", + "releaseDates": { + "2026-06": "2026-08-07" + }, + "extras": { + "targetUnit": "percent_growth", + "sourceBinding": { + "adapter": "alfred-fred", + "sourceUrl": "https://alfred.stlouisfed.org/graph/alfredgraph.csv?id=NONREVSLAR", + "sourceSeriesId": "NONREVSLAR", + "field": "NONREVSLAR", + "table": "G.19 Consumer Credit, outstanding, seasonally adjusted", + "transform": { + "operation": "multiply", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "bls.cpi.shelter_mom", + "cadence": "monthly", + "slug": "us-cpi-shelter-mom-{month}-{year}", + "seedPeriod": "2026-07", + "releaseCalendarUrl": "https://www.bls.gov/schedule/news_release/cpi.htm", + "releaseDates": { + "2026-07": "2026-08-12" + }, + "extras": { + "targetUnit": "percent_growth", + "sourceBinding": { + "adapter": "alfred-fred", + "sourceUrl": "https://alfred.stlouisfed.org/graph/alfredgraph.csv?id=CUSR0000SAH1", + "sourceSeriesId": "CUSR0000SAH1", + "field": "CUSR0000SAH1", + "table": "Consumer Price Index, U.S. city average, monthly seasonally adjusted", + "transform": { + "operation": "multiply", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "bls.cpi.rent_primary_residence_mom", + "cadence": "monthly", + "slug": "us-cpi-primary-rent-mom-{month}-{year}", + "seedPeriod": "2026-07", + "releaseCalendarUrl": "https://www.bls.gov/schedule/news_release/cpi.htm", + "releaseDates": { + "2026-07": "2026-08-12" + }, + "extras": { + "targetUnit": "percent_growth", + "sourceBinding": { + "adapter": "alfred-fred", + "sourceUrl": "https://alfred.stlouisfed.org/graph/alfredgraph.csv?id=CUSR0000SEHA", + "sourceSeriesId": "CUSR0000SEHA", + "field": "CUSR0000SEHA", + "table": "Consumer Price Index, U.S. city average, monthly seasonally adjusted", + "transform": { + "operation": "multiply", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "bls.cpi.owners_equivalent_rent_mom", + "cadence": "monthly", + "slug": "us-cpi-owners-equivalent-rent-mom-{month}-{year}", + "seedPeriod": "2026-07", + "releaseCalendarUrl": "https://www.bls.gov/schedule/news_release/cpi.htm", + "releaseDates": { + "2026-07": "2026-08-12" + }, + "extras": { + "targetUnit": "percent_growth", + "sourceBinding": { + "adapter": "alfred-fred", + "sourceUrl": "https://alfred.stlouisfed.org/graph/alfredgraph.csv?id=CUSR0000SEHC", + "sourceSeriesId": "CUSR0000SEHC", + "field": "CUSR0000SEHC", + "table": "Consumer Price Index, U.S. city average, monthly seasonally adjusted", + "transform": { + "operation": "multiply", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "bls.cpi.services_less_energy_mom", + "cadence": "monthly", + "slug": "us-cpi-services-less-energy-mom-{month}-{year}", + "seedPeriod": "2026-07", + "releaseCalendarUrl": "https://www.bls.gov/schedule/news_release/cpi.htm", + "releaseDates": { + "2026-07": "2026-08-12" + }, + "extras": { + "targetUnit": "percent_growth", + "sourceBinding": { + "adapter": "alfred-fred", + "sourceUrl": "https://alfred.stlouisfed.org/graph/alfredgraph.csv?id=CUSR0000SASLE", + "sourceSeriesId": "CUSR0000SASLE", + "field": "CUSR0000SASLE", + "table": "Consumer Price Index, U.S. city average, monthly seasonally adjusted", + "transform": { + "operation": "multiply", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "bls.cpi.services_less_rent_shelter_mom", + "cadence": "monthly", + "slug": "us-cpi-services-less-rent-shelter-mom-{month}-{year}", + "seedPeriod": "2026-07", + "releaseCalendarUrl": "https://www.bls.gov/schedule/news_release/cpi.htm", + "releaseDates": { + "2026-07": "2026-08-12" + }, + "extras": { + "targetUnit": "percent_growth", + "sourceBinding": { + "adapter": "alfred-fred", + "sourceUrl": "https://alfred.stlouisfed.org/graph/alfredgraph.csv?id=CUSR0000SASL2RS", + "sourceSeriesId": "CUSR0000SASL2RS", + "field": "CUSR0000SASL2RS", + "table": "Consumer Price Index, U.S. city average, monthly seasonally adjusted", + "transform": { + "operation": "multiply", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "bls.jolts.hires_rate", + "cadence": "monthly", + "slug": "jolts-hires-rate-{month}-{year}", + "seedPeriod": "2026-06", + "releaseCalendarUrl": "https://www.bls.gov/schedule/news_release/jolts.htm", + "releaseDates": { + "2026-06": "2026-08-04" + }, + "extras": { + "targetUnit": "percent", + "sourceBinding": { + "adapter": "alfred-fred", + "sourceUrl": "https://alfred.stlouisfed.org/graph/alfredgraph.csv?id=JTSHIR", + "sourceSeriesId": "JTSHIR", + "field": "JTSHIR", + "table": "JOLTS news release, Table 1", + "transform": { + "operation": "multiply", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "bls.ces.average_hourly_earnings_private", + "cadence": "monthly", + "slug": "average-hourly-earnings-mom-{month}-{year}", + "extras": { + "targetUnit": "percent_growth", + "sourceBinding": { + "adapter": "alfred-fred", + "sourceUrl": "https://alfred.stlouisfed.org/graph/alfredgraph.csv?id=CES0500000003", + "sourceSeriesId": "CES0500000003", + "field": "CES0500000003", + "table": "Employment Situation, Table B-3", + "transform": { + "operation": "multiply", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "bls.lns11300000", + "cadence": "monthly", + "slug": "labor-force-participation-{month_abbr}-{year}", + "extras": { + "targetUnit": "percent", + "sourceBinding": { + "adapter": "alfred-fred", + "sourceUrl": "https://alfred.stlouisfed.org/graph/alfredgraph.csv?id=CIVPART", + "sourceSeriesId": "CIVPART", + "field": "CIVPART", + "table": "Employment Situation, Table A-1", + "transform": { + "operation": "multiply", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "bls.cps.u6_underemployment_rate", + "cadence": "monthly", + "slug": "u6-underemployment-rate-{month}-{year}", + "seedPeriod": "2026-07", + "releaseCalendarUrl": "https://www.bls.gov/schedule/news_release/empsit.htm", + "releaseDates": { + "2026-07": "2026-08-07" + }, + "extras": { + "targetUnit": "percent", + "sourceBinding": { + "adapter": "alfred-fred", + "sourceUrl": "https://alfred.stlouisfed.org/graph/alfredgraph.csv?id=U6RATE", + "sourceSeriesId": "U6RATE", + "field": "U6RATE", + "table": "Employment Situation, Table A-15", + "transform": { + "operation": "multiply", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "usaspending.dod.prime_award_obligations", + "cadence": "annual", + "period": "FY2026", + "slug": "us-dod-prime-award-obligations-{period}", + "extras": { + "expectedReleaseWindow": { + "start": "2026-10-15", + "end": "2026-10-22" + }, + "valueScale": 1e-09, + "targetUnit": "billions USD", + "sourceBinding": { + "adapter": "usaspending-api", + "sourceUrl": "https://api.usaspending.gov/api/v2/agency/097/awards/?fiscal_year={fiscal_year}", + "sourceSeriesId": "usaspending.agency.097.awards.obligations", + "field": "obligations", + "table": "USAspending API v2, agency 097 (DoD) award summary, prime award obligations, fiscal year to date", + "transform": { + "operation": "multiply", + "factor": 1e-09 + }, + "releasePolicy": "registered_query_snapshot" + } + } + }, + { + "series": "usaspending.dod.prime_contract_obligations", + "cadence": "annual", + "period": "FY2026", + "slug": "us-dod-prime-contract-obligations-{period}", + "extras": { + "expectedReleaseWindow": { + "start": "2026-10-15", + "end": "2026-10-22" + }, + "valueScale": 1e-09, + "targetUnit": "billions USD", + "sourceBinding": { + "adapter": "usaspending-api", + "sourceUrl": "https://api.usaspending.gov/api/v2/agency/097/obligations_by_award_category/?fiscal_year={fiscal_year}", + "sourceSeriesId": "usaspending.agency.097.obligations_by_award_category.contracts", + "field": "results[category=contracts].aggregated_amount", + "table": "USAspending API v2, agency 097 (DoD) obligations by award category, contracts row, fiscal year to date", + "transform": { + "operation": "multiply", + "factor": 1e-09 + }, + "releasePolicy": "registered_query_snapshot" + } + } + }, + { + "series": "usaspending.dod.new_prime_awards", + "cadence": "annual", + "period": "FY2026", + "slug": "us-dod-new-prime-awards-{period}", + "extras": { + "expectedReleaseWindow": { + "start": "2026-10-15", + "end": "2026-10-22" + }, + "valueScale": 1e-06, + "targetUnit": "millions", + "sourceBinding": { + "adapter": "usaspending-api", + "sourceUrl": "https://api.usaspending.gov/api/v2/agency/097/awards/new/count/?fiscal_year={fiscal_year}", + "sourceSeriesId": "usaspending.agency.097.awards.new_award_count", + "field": "new_award_count", + "table": "USAspending API v2, agency 097 (DoD) new award count, fiscal year to date", + "transform": { + "operation": "multiply", + "factor": 1e-06 + }, + "releasePolicy": "registered_query_snapshot" + } + } + }, + { + "series": "usaspending.dod.prime_award_transactions", + "cadence": "annual", + "period": "FY2026", + "slug": "us-dod-prime-award-transactions-{period}", + "extras": { + "expectedReleaseWindow": { + "start": "2026-10-15", + "end": "2026-10-22" + }, + "valueScale": 1e-06, + "targetUnit": "millions", + "sourceBinding": { + "adapter": "usaspending-api", + "sourceUrl": "https://api.usaspending.gov/api/v2/agency/097/awards/?fiscal_year={fiscal_year}", + "sourceSeriesId": "usaspending.agency.097.awards.transaction_count", + "field": "transaction_count", + "table": "USAspending API v2, agency 097 (DoD) award summary, transaction count, fiscal year to date", + "transform": { + "operation": "multiply", + "factor": 1e-06 + }, + "releasePolicy": "registered_query_snapshot" + } + } + }, + { + "series": "usaspending.dod.unique_prime_contract_recipients", + "cadence": "annual", + "period": "FY2026", + "slug": "us-dod-unique-prime-contract-recipients-{period}", + "extras": { + "expectedReleaseWindow": { + "start": "2026-10-15", + "end": "2026-10-22" + }, + "valueScale": 0.001, + "targetUnit": "thousands", + "sourceBinding": { + "adapter": "usaspending-api", + "sourceUrl": "https://api.usaspending.gov/api/v2/search/spending_by_category/recipient/", + "sourceSeriesId": "usaspending.search.spending_by_category.recipient.dod.contracts.distinct", + "field": "results[].recipient_id", + "table": "USAspending API v2 advanced search, DoD prime-contract obligations grouped by recipient, fiscal year to date", + "transform": { + "operation": "count_distinct", + "requestMethod": "POST", + "fiscalYear": "{fiscal_year}", + "spendingLevel": "transactions", + "agency": { + "type": "awarding", + "tier": "toptier", + "name": "Department of Defense" + }, + "awardTypeCodes": [ + "A", + "B", + "C", + "D" + ], + "identityField": "recipient_id", + "excludeNullIdentity": true, + "pageSize": 100, + "factor": 0.001 + }, + "releasePolicy": "registered_query_snapshot" + } + } + }, + { + "series": "usaspending.dod.small_business_contract_obligation_share", + "cadence": "annual", + "period": "FY2026", + "slug": "us-dod-small-business-contract-obligation-share-{period}", + "extras": { + "expectedReleaseWindow": { + "start": "2026-10-15", + "end": "2026-10-22" + }, + "valueScale": 1, + "targetUnit": "percent", + "sourceBinding": { + "adapter": "usaspending-api", + "sourceUrl": "https://api.usaspending.gov/api/v2/search/spending_over_time/", + "sourceSeriesId": "usaspending.search.spending_over_time.dod.contracts.small_business_obligation_share", + "field": "results[time_period.fiscal_year={fiscal_year}].aggregated_amount", + "table": "USAspending API v2 advanced search, small-business share of DoD prime-contract obligations, fiscal year to date", + "transform": { + "operation": "ratio_percent", + "requestMethod": "POST", + "fiscalYear": "{fiscal_year}", + "group": "fiscal_year", + "spendingLevel": "transactions", + "agency": { + "type": "awarding", + "tier": "toptier", + "name": "Department of Defense" + }, + "awardTypeCodes": [ + "A", + "B", + "C", + "D" + ], + "numeratorRecipientTypeNames": [ + "small_business" + ], + "denominatorRecipientTypeNames": [], + "factor": 1 + }, + "releasePolicy": "registered_query_snapshot" + } + } + }, + { + "series": "bls.cps.employed_people_by_occupation.business_financial_operations", + "cadence": "monthly", + "slug": "cps-business-financial-employment-{month}-{year}", + "extras": { + "valueScale": 0.001, + "targetUnit": "millions", + "sourceBinding": { + "adapter": "generic-url", + "sourceUrl": "https://www.bls.gov/web/empsit/cpseea19.htm", + "sourceSeriesId": "bls.cps.employed_people_by_occupation.business_financial_operations", + "field": "Business and financial operations occupations", + "table": "CPS Employment Situation Table A-19, employed persons by occupation, not seasonally adjusted (thousands)", + "transform": { + "operation": "multiply", + "factor": 0.001 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "bls.cps.employed_people_by_occupation.computer_mathematical", + "cadence": "monthly", + "slug": "cps-computer-math-employment-{month}-{year}", + "extras": { + "valueScale": 0.001, + "targetUnit": "millions", + "sourceBinding": { + "adapter": "generic-url", + "sourceUrl": "https://www.bls.gov/web/empsit/cpseea19.htm", + "sourceSeriesId": "bls.cps.employed_people_by_occupation.computer_mathematical", + "field": "Computer and mathematical occupations", + "table": "CPS Employment Situation Table A-19, employed persons by occupation, not seasonally adjusted (thousands)", + "transform": { + "operation": "multiply", + "factor": 0.001 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "bls.cps.employed_people_by_occupation.healthcare_support", + "cadence": "monthly", + "slug": "cps-healthcare-support-employment-{month}-{year}", + "extras": { + "valueScale": 0.001, + "targetUnit": "millions", + "sourceBinding": { + "adapter": "generic-url", + "sourceUrl": "https://www.bls.gov/web/empsit/cpseea19.htm", + "sourceSeriesId": "bls.cps.employed_people_by_occupation.healthcare_support", + "field": "Healthcare support occupations", + "table": "CPS Employment Situation Table A-19, employed persons by occupation, not seasonally adjusted (thousands)", + "transform": { + "operation": "multiply", + "factor": 0.001 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "bls.cps.employed_people_by_occupation.office_administrative_support", + "cadence": "monthly", + "slug": "cps-office-admin-employment-{month}-{year}", + "extras": { + "valueScale": 0.001, + "targetUnit": "millions", + "sourceBinding": { + "adapter": "generic-url", + "sourceUrl": "https://www.bls.gov/web/empsit/cpseea19.htm", + "sourceSeriesId": "bls.cps.employed_people_by_occupation.office_administrative_support", + "field": "Office and administrative support occupations", + "table": "CPS Employment Situation Table A-19, employed persons by occupation, not seasonally adjusted (thousands)", + "transform": { + "operation": "multiply", + "factor": 0.001 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "bls.cps.employed_people_by_occupation.production", + "cadence": "monthly", + "slug": "cps-production-employment-{month}-{year}", + "extras": { + "valueScale": 0.001, + "targetUnit": "millions", + "sourceBinding": { + "adapter": "generic-url", + "sourceUrl": "https://www.bls.gov/web/empsit/cpseea19.htm", + "sourceSeriesId": "bls.cps.employed_people_by_occupation.production", + "field": "Production occupations", + "table": "CPS Employment Situation Table A-19, employed persons by occupation, not seasonally adjusted (thousands)", + "transform": { + "operation": "multiply", + "factor": 0.001 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "bls.cps.employed_people_by_occupation.transportation_material_moving", + "cadence": "monthly", + "slug": "cps-transport-material-moving-employment-{month}-{year}", + "extras": { + "valueScale": 0.001, + "targetUnit": "millions", + "sourceBinding": { + "adapter": "generic-url", + "sourceUrl": "https://www.bls.gov/web/empsit/cpseea19.htm", + "sourceSeriesId": "bls.cps.employed_people_by_occupation.transportation_material_moving", + "field": "Transportation and material moving occupations", + "table": "CPS Employment Situation Table A-19, employed persons by occupation, not seasonally adjusted (thousands)", + "transform": { + "operation": "multiply", + "factor": 0.001 + }, + "releasePolicy": "first_print" + } + } + }, + { + "series": "abs.labour.employment_change.australia", + "cadence": "monthly", + "slug": "abs-labour-employment-change-australia-{month}-{year}", + "extras": { + "valueScale": 1, + "targetUnit": "thousands", + "country": "AU", + "sourceBinding": { + "adapter": "generic-url", + "field": "LF/M3.3.1599.20.AUS.M", + "releasePolicy": "first_print", + "sourceSeriesId": "LF/M3.3.1599.20.AUS.M", + "sourceUrl": "https://data.api.abs.gov.au/rest/data/LF/M3.3.1599.20.AUS.M?lastNObservations=30&format=jsondata", + "table": "Labour Force, Australia (dataflow LF: employed persons, seasonally adjusted, Australia; month-over-month change)", + "transform": { + "factor": 1, + "operation": "identity" + } + } + } + }, + { + "series": "usda.fsa.crp.enrolled_acres_total", + "cadence": "monthly", + "slug": "us-crp-enrolled-acres-{month}-{year}", + "extras": { + "valueScale": 1, + "targetUnit": "count", + "country": "US", + "sourceBinding": { + "adapter": "fsa-crp-monthly-summary", + "sourceUrl": "https://www.fsa.usda.gov/resources/programs/conservation-reserve-program/statistics", + "sourceSeriesId": "usda.fsa.crp.enrolled_acres_total", + "field": "enrolled_acres_total", + "table": "USDA FSA Conservation Reserve Program Statistics, CRP Monthly Summary, total row", + "transform": { + "operation": "identity", + "factor": 1 + }, + "releasePolicy": "first_print" + } + } + } + ] +} diff --git a/ledger/series_catalog.json b/ledger/series_catalog.json index caeea50..557fad4 100644 --- a/ledger/series_catalog.json +++ b/ledger/series_catalog.json @@ -1,11 +1,21 @@ { - "comment": "Canonical series catalog. One row per series family; uuid is minted once and never re-minted (regeneration preserves it by concept). Consumers reference series by uuid or concept only. Regenerate with scripts/build_series_catalog.py; verify with --check. Cross-spelling merges are manual curation: keep the surviving row's uuid, move absorbed spellings to aliases.", - "generator_version": 1, + "comment": "Canonical series catalog. One row per (concept, geography, entity) identity; uuid is minted once and never re-minted (regeneration preserves it by identity, then by concept/alias). Consumers reference series by uuid or concept only. Regenerate with scripts/build_series_catalog.py; verify with --check. Cross-spelling and cross-vintage merges are manual curation: keep the surviving row's uuid, move absorbed spellings to aliases — curated aliases persist across regeneration. Aliases listed in ambiguous_aliases match multiple rows and never drive identity inheritance.", + "generator_version": 2, "observations_sha256": "63127ff427a4aa3884f54dd1ee070ab631c15ebb38f23fdc9095a45e9204109f", "observation_rows": 168, + "docket_seed_sha256": "930424fb48c0be4c9e2ce17d4e0f2a6be886408e814c80324174a7a303fa0271", + "suspect_segments": [], + "ambiguous_aliases": [ + "CPI/3.10001.10.50.M", + "JTSJOL", + "PCEPILFE", + "prc_hicp_minr/M.RCH_A.TOTAL.EA21", + "v41690973", + "v65201210" + ], "series": [ { - "uuid": "0a67f2eb-4776-416e-a9d6-8790e4d40d3d", + "uuid": "e344ff46-5b07-401e-9203-72fe4d643aef", "concept": "abs.building_approvals.total_dwellings_mom.australia", "family_patterns": [ "abs.building_approvals.total_dwellings_mom.australia.{P}" @@ -14,8 +24,9 @@ "unit": "percent_growth", "cadence": "month", "geography": { - "id": "AU", "level": "country", + "id": "AU", + "vintage": "current", "name": "Australia" }, "entity": { @@ -37,7 +48,7 @@ "observation_count": 1 }, { - "uuid": "cb0a5069-e734-4351-a07b-b3c1d3b30c6d", + "uuid": "17f78cc1-f82a-44a6-9620-fc10c310f8f1", "concept": "abs.cpi.all_groups.yoy", "family_patterns": [ "abs.cpi.all_groups.yoy" @@ -46,8 +57,9 @@ "unit": "percent", "cadence": "month", "geography": { - "id": "AU", "level": "country", + "id": "AU", + "vintage": "current", "name": "Australia" }, "entity": { @@ -68,7 +80,7 @@ "observation_count": 1 }, { - "uuid": "b38445cc-367f-47ca-9bb2-393890aa4d14", + "uuid": "b4935b21-ee8b-4076-b498-7babcf57ba07", "concept": "abs.cpi.all_groups_annual_rate.australia", "family_patterns": [ "abs.cpi.all_groups_annual_rate.australia.{P}" @@ -77,8 +89,9 @@ "unit": "percent", "cadence": "month", "geography": { - "id": "AU", "level": "country", + "id": "AU", + "vintage": "current", "name": "Australia" }, "entity": { @@ -100,7 +113,7 @@ "observation_count": 1 }, { - "uuid": "11bb2c93-9bf0-47fa-8609-cf88277b1c01", + "uuid": "977a5286-88de-4ebe-82aa-2d3bca2d8360", "concept": "abs.cpi_indicator.allgroups.yoy", "family_patterns": [ "abs.cpi_indicator.allgroups.yoy.{P}" @@ -109,8 +122,9 @@ "unit": "percent", "cadence": "month", "geography": { - "id": "AU", "level": "country", + "id": "AU", + "vintage": "current", "name": "Australia" }, "entity": { @@ -132,7 +146,7 @@ "observation_count": 1 }, { - "uuid": "73365ddc-a6c2-4d27-af69-dcfc5cea5cfd", + "uuid": "b17f788a-411a-4d9c-bb45-68d412eddeed", "concept": "abs.labour.employment_change.australia", "family_patterns": [ "abs.labour.employment_change.australia.{P}" @@ -141,8 +155,9 @@ "unit": "thousands", "cadence": "month", "geography": { - "id": "AU", "level": "country", + "id": "AU", + "vintage": "current", "name": "Australia" }, "entity": { @@ -165,7 +180,7 @@ "observation_count": 2 }, { - "uuid": "843e5bab-22f6-44ad-9df2-8f1e43882ce6", + "uuid": "f9067f72-71de-469c-8d18-c838e8ff0859", "concept": "abs.labour.unemployment_rate", "family_patterns": [ "abs.labour.unemployment_rate" @@ -183,7 +198,7 @@ "observation_count": 0 }, { - "uuid": "07353377-eccd-4023-b847-8866a3ad386a", + "uuid": "15214884-48b0-49f1-9d17-4f9b60e96fce", "concept": "abs.labour.unemployment_rate.australia", "family_patterns": [ "abs.labour.unemployment_rate.australia.{P}" @@ -192,8 +207,9 @@ "unit": "percent", "cadence": "month", "geography": { - "id": "AU", "level": "country", + "id": "AU", + "vintage": "current", "name": "Australia" }, "entity": { @@ -216,17 +232,18 @@ "observation_count": 2 }, { - "uuid": "ae863b3f-47fd-4585-b5ea-6beb8e6ceb60", - "concept": "bank_of_canada.overnight_rate.after_june_2026", + "uuid": "8f65877d-6acb-4ee6-9158-b5448c526f21", + "concept": "bank_of_canada.overnight_rate", "family_patterns": [ - "bank_of_canada.overnight_rate.after_june_2026" + "bank_of_canada.overnight_rate.{P}" ], "status": "observed", "unit": "percent", "cadence": "month", "geography": { - "id": "CA", "level": "country", + "id": "CA", + "vintage": "current", "name": "Canada" }, "entity": { @@ -236,16 +253,18 @@ "sources": [ "bank_of_canada" ], - "aliases": [], - "rid_patterns": [ + "aliases": [ "bank_of_canada.overnight_rate.after_june_2026" ], + "rid_patterns": [ + "bank_of_canada.overnight_rate.{P}" + ], "first_observed_period": "2026-06", "last_observed_period": "2026-06", "observation_count": 1 }, { - "uuid": "019a338e-df79-4c56-af6a-8ea5c610ab9d", + "uuid": "2b523a47-f470-4dc3-8d15-4797a709079d", "concept": "bea.core_pce.mom", "family_patterns": [ "bea.core_pce.mom" @@ -263,7 +282,7 @@ "observation_count": 0 }, { - "uuid": "f5b03100-26d3-47a2-82f5-ad833e4097de", + "uuid": "ae903852-8f94-4f58-ab5e-27068c9d46e1", "concept": "bea.disposable_personal_income.level", "family_patterns": [ "bea.disposable_personal_income.level.{P}" @@ -272,8 +291,9 @@ "unit": "usd_billions", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -295,7 +315,7 @@ "observation_count": 1 }, { - "uuid": "5d4a2990-6fb8-4517-b4d1-df90bf97b0e1", + "uuid": "c4172a11-ef6e-42eb-89c0-00da23b825e1", "concept": "bea.government_social_benefits.level", "family_patterns": [ "bea.government_social_benefits.level.{P}" @@ -304,8 +324,9 @@ "unit": "usd_billions", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -327,7 +348,7 @@ "observation_count": 1 }, { - "uuid": "8d836dd8-5942-4a56-b554-0bb44554a2fc", + "uuid": "540cce9c-6111-4547-a248-cc786deb8e1c", "concept": "bea.government_social_benefits.medicaid", "family_patterns": [ "bea.government_social_benefits.medicaid.{P}" @@ -336,8 +357,9 @@ "unit": "usd_billions", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -359,7 +381,7 @@ "observation_count": 1 }, { - "uuid": "4eb192e1-d663-482d-b960-2c04ab901f07", + "uuid": "b96f7aad-ffee-4849-a032-1343728c321d", "concept": "bea.government_social_benefits.medicare", "family_patterns": [ "bea.government_social_benefits.medicare.{P}" @@ -368,8 +390,9 @@ "unit": "usd_billions", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -391,7 +414,7 @@ "observation_count": 1 }, { - "uuid": "107564f7-ee16-463e-b1a7-7b109a2f3763", + "uuid": "73f44532-772a-428e-a255-08699bdb8f4e", "concept": "bea.government_social_benefits.social_security", "family_patterns": [ "bea.government_social_benefits.social_security.{P}" @@ -400,8 +423,9 @@ "unit": "usd_billions", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -423,7 +447,7 @@ "observation_count": 1 }, { - "uuid": "0f56dfe3-2cdd-4f81-9264-fefb74f022d0", + "uuid": "db938f25-1086-4faa-a9b6-dc378fd35ad8", "concept": "bea.pce.core_mom", "family_patterns": [ "bea.pce.core_mom.{P}" @@ -432,8 +456,9 @@ "unit": "percent_growth", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -455,7 +480,7 @@ "observation_count": 1 }, { - "uuid": "7e8c7edc-0aeb-4863-ada6-c98445fb835f", + "uuid": "622be0bf-ddaf-4051-a1ea-58dc4a34a67b", "concept": "bea.pce_price_index.monthly_change", "family_patterns": [ "bea.pce_price_index.monthly_change.{P}" @@ -464,8 +489,9 @@ "unit": "percent_growth", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -487,7 +513,7 @@ "observation_count": 1 }, { - "uuid": "5aaf7212-b599-4d52-b233-06d82bafee3d", + "uuid": "33f11e9e-085e-48e0-abb1-3a85f85f510c", "concept": "bea.personal_current_taxes.level", "family_patterns": [ "bea.personal_current_taxes.level.{P}" @@ -496,8 +522,9 @@ "unit": "usd_billions", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -519,7 +546,7 @@ "observation_count": 1 }, { - "uuid": "a2ab0909-0db8-4ba9-b1d3-1d109b182334", + "uuid": "1467ba0f-93fd-4727-96e7-09954992831b", "concept": "bea.real_gdp.saar.third_estimate", "family_patterns": [ "bea.real_gdp.saar.{P}.third_estimate" @@ -528,8 +555,9 @@ "unit": "percent_growth", "cadence": "quarter", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -551,7 +579,7 @@ "observation_count": 1 }, { - "uuid": "e303a245-6b71-466d-a25a-343c2e51d09e", + "uuid": "1eabcc3f-c99a-4217-ba36-dcaeef5aef60", "concept": "bea.trade.goods_services_deficit", "family_patterns": [ "bea.trade.goods_services_deficit" @@ -569,7 +597,7 @@ "observation_count": 0 }, { - "uuid": "63dca7c9-7eb5-4a42-9737-90c0a0bdb11d", + "uuid": "fa09cffe-676f-4c84-8064-e9a7960338ad", "concept": "bea.wages_and_salaries.level", "family_patterns": [ "bea.wages_and_salaries.level.{P}" @@ -578,8 +606,9 @@ "unit": "usd_billions", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -601,7 +630,7 @@ "observation_count": 1 }, { - "uuid": "ccd49c74-dcde-4ab6-bb40-d90a035a3b67", + "uuid": "5a39ebcb-26da-46f3-9483-b8ba708a776e", "concept": "bls.ces.average_hourly_earnings_private_monthly_change", "family_patterns": [ "bls.ces.average_hourly_earnings_private_monthly_change" @@ -610,8 +639,9 @@ "unit": "percent_growth", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -632,7 +662,7 @@ "observation_count": 1 }, { - "uuid": "39252487-0710-42df-8cbe-e4ca60ff31c0", + "uuid": "11271834-da12-4926-a4ce-9f996fb6e7df", "concept": "bls.ces.nonfarm_payrolls.change", "family_patterns": [ "bls.ces.nonfarm_payrolls.change" @@ -650,26 +680,25 @@ "observation_count": 0 }, { - "uuid": "b2d62920-5414-4e83-b6ed-91fa84fda47b", + "uuid": "88bb402f-81ab-43e1-b0d4-a3707d2cf10f", "concept": "bls.ces.total_nonfarm_payroll_change", "family_patterns": [ - "bls.ces.total_nonfarm_payroll_change", "bls.ces.total_nonfarm_payroll_change.{P}" ], "status": "observed", "unit": "thousands", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { - "name": "person", - "role": "nonfarm_payroll_employee" + "name": "economy", + "role": "aggregate" }, "sources": [ - "bls", "bls_ces" ], "aliases": [ @@ -679,12 +708,42 @@ "rid_patterns": [ "bls.ces.total_nonfarm_payroll_change.{P}.first_print" ], - "first_observed_period": "2026-05", + "first_observed_period": "2026-06", "last_observed_period": "2026-06", - "observation_count": 2 + "observation_count": 1 + }, + { + "uuid": "8735586f-8f28-43b3-92bf-d5f4f0cf06c8", + "concept": "bls.ces.total_nonfarm_payroll_change", + "family_patterns": [ + "bls.ces.total_nonfarm_payroll_change" + ], + "status": "observed", + "unit": "thousands", + "cadence": "month", + "geography": { + "level": "country", + "id": "0100000US", + "vintage": "current", + "name": "United States" + }, + "entity": { + "name": "person", + "role": "nonfarm_payroll_employee" + }, + "sources": [ + "bls" + ], + "aliases": [], + "rid_patterns": [ + "bls.ces.total_nonfarm_payroll_change.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 }, { - "uuid": "17d3f396-e7cf-4955-8c06-b7370df4c735", + "uuid": "1678deac-7af3-4012-92d8-ca70d4e4ad8b", "concept": "bls.cpi.owners_equivalent_rent_mom", "family_patterns": [ "bls.cpi.owners_equivalent_rent_mom" @@ -702,7 +761,7 @@ "observation_count": 0 }, { - "uuid": "3417de03-198f-4a6e-a2f8-f59a357c9e4a", + "uuid": "f21957d5-0017-4a38-9ec0-81dc8ee06b75", "concept": "bls.cpi.rent_primary_residence_mom", "family_patterns": [ "bls.cpi.rent_primary_residence_mom" @@ -720,7 +779,7 @@ "observation_count": 0 }, { - "uuid": "05e7a6ae-90c4-40f5-ab38-2426f213499d", + "uuid": "35f5c356-4da0-488f-8d4e-4edcf1c76536", "concept": "bls.cpi.services_less_energy_mom", "family_patterns": [ "bls.cpi.services_less_energy_mom" @@ -738,7 +797,7 @@ "observation_count": 0 }, { - "uuid": "173a2bb9-97c3-4a00-a4e8-9ccb7288d5dd", + "uuid": "13afafd9-60cc-41e7-a3eb-2e6e601bae64", "concept": "bls.cpi.services_less_rent_shelter_mom", "family_patterns": [ "bls.cpi.services_less_rent_shelter_mom" @@ -756,7 +815,7 @@ "observation_count": 0 }, { - "uuid": "41881396-3fb0-4a1f-bac9-6c8c58f73d20", + "uuid": "e26bb549-b24e-4971-93d4-4794f70d87d1", "concept": "bls.cpi.shelter_mom", "family_patterns": [ "bls.cpi.shelter_mom" @@ -774,7 +833,7 @@ "observation_count": 0 }, { - "uuid": "23a58ec2-2d41-4f17-ae3d-8affeda44fc1", + "uuid": "64198097-ede9-477c-8f3c-d5b247b2dae4", "concept": "bls.cpi.u.core_mom", "family_patterns": [ "bls.cpi.u.core_mom.{P}" @@ -783,8 +842,42 @@ "unit": "percent_growth", "cadence": "month", "geography": { + "level": "country", "id": "0100000US", + "vintage": "current", + "name": "United States" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "bls_cpi" + ], + "aliases": [ + "CPILFESL", + "bls.cpi.u.core_mom.june_2026" + ], + "rid_patterns": [ + "bls.cpi.u.core_mom.{P}.first_print" + ], + "first_observed_period": "2026-06", + "last_observed_period": "2026-06", + "observation_count": 1 + }, + { + "uuid": "f98732c4-59d1-4bb1-94dd-e23ea9db3775", + "concept": "bls.cpi.u.core_mom", + "family_patterns": [ + "bls.cpi.u.core_mom.{P}" + ], + "status": "observed", + "unit": "percent_growth", + "cadence": "month", + "geography": { "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -792,23 +885,20 @@ "role": "cpi_u_less_food_energy" }, "sources": [ - "bls", - "bls_cpi" + "bls" ], "aliases": [ - "CPILFESL", - "bls.cpi.u.core_mom.june_2026", "bls.cpi.u.core_mom.may_2026" ], "rid_patterns": [ "bls.cpi.u.core_mom.{P}.first_print" ], "first_observed_period": "2026-05", - "last_observed_period": "2026-06", - "observation_count": 2 + "last_observed_period": "2026-05", + "observation_count": 1 }, { - "uuid": "e6cf3897-504a-4fd2-b314-24c38e13cafb", + "uuid": "4b37d4ee-446a-449c-be74-59177dec5d25", "concept": "bls.cpi.u.headline_mom", "family_patterns": [ "bls.cpi.u.headline_mom.{P}" @@ -817,8 +907,42 @@ "unit": "percent_growth", "cadence": "month", "geography": { + "level": "country", "id": "0100000US", + "vintage": "current", + "name": "United States" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "bls_cpi" + ], + "aliases": [ + "CPIAUCSL", + "bls.cpi.u.headline_mom.june_2026" + ], + "rid_patterns": [ + "bls.cpi.u.headline_mom.{P}.first_print" + ], + "first_observed_period": "2026-06", + "last_observed_period": "2026-06", + "observation_count": 1 + }, + { + "uuid": "41b3f275-2b6a-4e5b-a70a-a5dae8c83be6", + "concept": "bls.cpi.u.headline_mom", + "family_patterns": [ + "bls.cpi.u.headline_mom.{P}" + ], + "status": "observed", + "unit": "percent_growth", + "cadence": "month", + "geography": { "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -826,23 +950,20 @@ "role": "cpi_u_all_items" }, "sources": [ - "bls", - "bls_cpi" + "bls" ], "aliases": [ - "CPIAUCSL", - "bls.cpi.u.headline_mom.june_2026", "bls.cpi.u.headline_mom.may_2026" ], "rid_patterns": [ "bls.cpi.u.headline_mom.{P}.first_print" ], "first_observed_period": "2026-05", - "last_observed_period": "2026-06", - "observation_count": 2 + "last_observed_period": "2026-05", + "observation_count": 1 }, { - "uuid": "f6ef8b1d-b769-4e23-9fb6-e8c07df16c37", + "uuid": "13cb7ad3-eceb-4c76-964f-022171881849", "concept": "bls.cps.employed_people_by_occupation.business_financial_operations", "family_patterns": [ "bls.cps.employed_people_by_occupation.business_financial_operations.{P}" @@ -851,8 +972,9 @@ "unit": "thousands", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -874,7 +996,7 @@ "observation_count": 1 }, { - "uuid": "5d93431c-cfdc-45a2-b62e-8bbc9ce70f6d", + "uuid": "0284504d-9da8-44e2-b223-dfc2bac838a8", "concept": "bls.cps.employed_people_by_occupation.computer_mathematical", "family_patterns": [ "bls.cps.employed_people_by_occupation.computer_mathematical.{P}" @@ -883,8 +1005,9 @@ "unit": "thousands", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -906,7 +1029,7 @@ "observation_count": 1 }, { - "uuid": "27c44d4d-c9da-46b2-9e0c-17ef7397b53e", + "uuid": "d1c55677-6436-407b-9632-4d23389f9650", "concept": "bls.cps.employed_people_by_occupation.healthcare_support", "family_patterns": [ "bls.cps.employed_people_by_occupation.healthcare_support.{P}" @@ -915,8 +1038,9 @@ "unit": "thousands", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -938,7 +1062,7 @@ "observation_count": 1 }, { - "uuid": "cf7aed6d-c97d-45f0-823b-f8adcff63da0", + "uuid": "ec4f8edd-1a44-4f51-a74b-77aec571445e", "concept": "bls.cps.employed_people_by_occupation.office_administrative_support", "family_patterns": [ "bls.cps.employed_people_by_occupation.office_administrative_support.{P}" @@ -947,8 +1071,9 @@ "unit": "thousands", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -970,7 +1095,7 @@ "observation_count": 1 }, { - "uuid": "479d1718-942b-49b7-abae-e606cb088000", + "uuid": "ad9fb3ff-8e30-418b-837c-83b572fb526b", "concept": "bls.cps.employed_people_by_occupation.production", "family_patterns": [ "bls.cps.employed_people_by_occupation.production.{P}" @@ -979,8 +1104,9 @@ "unit": "thousands", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -1002,7 +1128,7 @@ "observation_count": 1 }, { - "uuid": "5aa9a7fe-e5fb-4e53-9b74-629ea99985f0", + "uuid": "747b0c83-5c43-463d-8f1c-804726d7e92f", "concept": "bls.cps.employed_people_by_occupation.transportation_material_moving", "family_patterns": [ "bls.cps.employed_people_by_occupation.transportation_material_moving.{P}" @@ -1011,8 +1137,9 @@ "unit": "thousands", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -1034,7 +1161,7 @@ "observation_count": 1 }, { - "uuid": "be1586b2-7c70-46a4-ad99-fc53800a8834", + "uuid": "fa85d459-e276-4aa7-a31e-c29c078ea8ef", "concept": "bls.cps.telework_share", "family_patterns": [ "bls.cps.telework_share" @@ -1052,7 +1179,7 @@ "observation_count": 0 }, { - "uuid": "81af2046-ee63-413d-b990-71b8717e2b8c", + "uuid": "1804da11-bfb6-4788-96ee-b966f0193e49", "concept": "bls.cps.u6_underemployment_rate", "family_patterns": [ "bls.cps.u6_underemployment_rate" @@ -1070,26 +1197,25 @@ "observation_count": 0 }, { - "uuid": "bbf6d6b8-82c8-4ae5-9b07-ca493b7d717c", + "uuid": "d5277474-5aa3-43ec-8985-e5abe6cc4210", "concept": "bls.cps.unemployment_rate", "family_patterns": [ - "bls.cps.unemployment_rate", "bls.cps.unemployment_rate.{P}" ], "status": "observed", "unit": "percent", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { - "name": "person", - "role": "civilian_labor_force" + "name": "economy", + "role": "aggregate" }, "sources": [ - "bls", "bls_cps" ], "aliases": [ @@ -1099,12 +1225,42 @@ "rid_patterns": [ "bls.cps.unemployment_rate.{P}.first_print" ], - "first_observed_period": "2026-05", + "first_observed_period": "2026-06", "last_observed_period": "2026-06", - "observation_count": 2 + "observation_count": 1 + }, + { + "uuid": "efa1b3c9-4826-4179-8b79-92cda290d009", + "concept": "bls.cps.unemployment_rate", + "family_patterns": [ + "bls.cps.unemployment_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "month", + "geography": { + "level": "country", + "id": "0100000US", + "vintage": "current", + "name": "United States" + }, + "entity": { + "name": "person", + "role": "civilian_labor_force" + }, + "sources": [ + "bls" + ], + "aliases": [], + "rid_patterns": [ + "bls.cps.unemployment_rate.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 }, { - "uuid": "3a7ced38-7a0b-476d-a15d-369bab07ae88", + "uuid": "cbc7c2f1-ba6f-4fd3-937e-1e1501a4d77e", "concept": "bls.eci.private_wages_salaries_qoq", "family_patterns": [ "bls.eci.private_wages_salaries_qoq.{P}" @@ -1113,8 +1269,9 @@ "unit": "percent_growth", "cadence": "quarter", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -1136,7 +1293,7 @@ "observation_count": 1 }, { - "uuid": "15d5c333-c7aa-4f17-8c2c-75bc7e1a6c25", + "uuid": "b1369ea9-86d6-4af7-b8f0-50717149c38f", "concept": "bls.eci.total_compensation_private_industry_qoq", "family_patterns": [ "bls.eci.total_compensation_private_industry_qoq.{P}" @@ -1145,8 +1302,9 @@ "unit": "percent_growth", "cadence": "quarter", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -1168,7 +1326,7 @@ "observation_count": 1 }, { - "uuid": "a2243a6f-a8b0-4b78-8671-97dfdc715158", + "uuid": "2392247f-91ea-4ecc-8b65-11bc3f88e220", "concept": "bls.export_prices.all_commodities_mom", "family_patterns": [ "bls.export_prices.all_commodities_mom" @@ -1186,7 +1344,7 @@ "observation_count": 0 }, { - "uuid": "7d4a1345-dbbe-486d-9a5d-6ba17e2b38aa", + "uuid": "5622048a-7755-4f46-824d-160794cd2542", "concept": "bls.import_price_index.all_imports_mom", "family_patterns": [ "bls.import_price_index.all_imports_mom.{P}" @@ -1195,8 +1353,42 @@ "unit": "percent_growth", "cadence": "month", "geography": { + "level": "country", "id": "0100000US", + "vintage": "current", + "name": "United States" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "bls_import_export_prices" + ], + "aliases": [ + "IR", + "bls.import_price_index.all_imports_mom.2026-06" + ], + "rid_patterns": [ + "bls.import_price_index.all_imports_mom.{P}.first_print" + ], + "first_observed_period": "2026-06", + "last_observed_period": "2026-06", + "observation_count": 1 + }, + { + "uuid": "b2b0a6cc-9a31-456a-a28d-9ef4fde19406", + "concept": "bls.import_price_index.all_imports_mom", + "family_patterns": [ + "bls.import_price_index.all_imports_mom.{P}" + ], + "status": "observed", + "unit": "percent_growth", + "cadence": "month", + "geography": { "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -1204,24 +1396,21 @@ "role": "all_imports" }, "sources": [ - "bls", - "bls_import_export_prices" + "bls" ], "aliases": [ - "IR", "bls.import_price_index.all_imports", - "bls.import_price_index.all_imports_mom.2026-06", "bls.import_price_index.all_imports_mom.may_2026" ], "rid_patterns": [ "bls.import_price_index.all_imports_mom.{P}.first_print" ], "first_observed_period": "2026-05", - "last_observed_period": "2026-06", - "observation_count": 2 + "last_observed_period": "2026-05", + "observation_count": 1 }, { - "uuid": "ea88768a-c4e8-439c-ad74-2f0fffeec569", + "uuid": "bef0c281-ae00-4af2-8f92-54b23938f887", "concept": "bls.jolts.hires_rate", "family_patterns": [ "bls.jolts.hires_rate" @@ -1239,7 +1428,7 @@ "observation_count": 0 }, { - "uuid": "fd2da098-04e6-41bf-a921-35b8d62b1bd2", + "uuid": "75f43d09-f7bd-42f9-98dd-79a20ab8e994", "concept": "bls.jolts.job_openings", "family_patterns": [ "bls.jolts.job_openings.{P}" @@ -1248,8 +1437,9 @@ "unit": "millions", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -1271,7 +1461,7 @@ "observation_count": 1 }, { - "uuid": "da0efb61-b35b-48fb-9304-d91b618f2098", + "uuid": "8c40edce-469c-4852-9564-e138932ce996", "concept": "bls.jolts.job_openings_total", "family_patterns": [ "bls.jolts.job_openings_total.{P}" @@ -1280,8 +1470,9 @@ "unit": "thousands", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -1303,7 +1494,7 @@ "observation_count": 1 }, { - "uuid": "09f175c0-15cc-4300-a675-e2ec9fdeabc0", + "uuid": "ebc8ff13-5db1-4bfc-a921-58a7bfe748fa", "concept": "bls.jolts.quits_rate", "family_patterns": [ "bls.jolts.quits_rate" @@ -1321,7 +1512,7 @@ "observation_count": 0 }, { - "uuid": "6ffad760-05ec-4272-98f0-265c13d63a14", + "uuid": "41380405-1391-475e-86ce-edc5fbf1ecf1", "concept": "bls.lns11300000", "family_patterns": [ "bls.lns11300000" @@ -1339,7 +1530,7 @@ "observation_count": 0 }, { - "uuid": "511937f5-55fc-4518-8e08-76601bdd0f69", + "uuid": "ef49ff78-b4ed-440d-97fa-4631b59f9a15", "concept": "bls.ppi.final_demand_monthly_change", "family_patterns": [ "bls.ppi.final_demand_monthly_change.{P}" @@ -1348,8 +1539,9 @@ "unit": "percent_growth", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -1370,7 +1562,7 @@ "observation_count": 1 }, { - "uuid": "58ed86d3-32a8-40ac-b6e9-3bc013810d22", + "uuid": "db5162f5-39d0-47a4-829d-f65fffd2b79a", "concept": "bls.productivity.nonfarm_qoq_prelim", "family_patterns": [ "bls.productivity.nonfarm_qoq_prelim" @@ -1388,7 +1580,7 @@ "observation_count": 0 }, { - "uuid": "9f801a0e-c87e-4b5f-979f-ca2838afc3d2", + "uuid": "14944fa7-9ceb-45c4-856d-a47d24cdcf08", "concept": "bls.productivity.nonfarm_unit_labor_costs_qoq_prelim", "family_patterns": [ "bls.productivity.nonfarm_unit_labor_costs_qoq_prelim" @@ -1406,7 +1598,7 @@ "observation_count": 0 }, { - "uuid": "0b2d9164-ddc3-436b-86ec-c508788622c9", + "uuid": "90bf8f8b-40b9-4b86-91b1-c64ecaf73bdb", "concept": "bls.real_earnings.avg_hourly_mom", "family_patterns": [ "bls.real_earnings.avg_hourly_mom" @@ -1424,17 +1616,18 @@ "observation_count": 0 }, { - "uuid": "718d2c3c-710f-4606-a9c4-c33a50270066", - "concept": "boe.bank_rate.2026_06_18", + "uuid": "27b594ab-ad4d-470d-aa70-99f159f978e1", + "concept": "boe.bank_rate", "family_patterns": [ - "boe.bank_rate.2026_06_18" + "boe.bank_rate.{P}" ], "status": "observed", "unit": "percent", "cadence": "month", "geography": { - "id": "GB", "level": "country", + "id": "GB", + "vintage": "current", "name": "United Kingdom" }, "entity": { @@ -1445,58 +1638,30 @@ "boe" ], "aliases": [ - "boe.bank_rate" + "boe.bank_rate.2026_06_18", + "boe.bank_rate.after_mpc_june_2026" ], "rid_patterns": [ - "boe.bank_rate.{P}" + "boe.bank_rate.{P}", + "boe.bank_rate.{P}.first_print" ], "first_observed_period": "2026-06", "last_observed_period": "2026-06", - "observation_count": 1 + "observation_count": 2 }, { - "uuid": "4daecbe3-afb1-4e97-9368-0e7d049d7cfa", - "concept": "boe.bank_rate.after_mpc_june_2026", + "uuid": "a27769be-70a4-40ba-9c2b-afd2689468a5", + "concept": "boj.policy_rate_guideline", "family_patterns": [ - "boe.bank_rate.after_mpc_june_2026" + "boj.policy_rate_guideline.{P}" ], "status": "observed", "unit": "percent", "cadence": "month", "geography": { - "id": "GB", "level": "country", - "name": "United Kingdom" - }, - "entity": { - "name": "government", - "role": "bank_rate" - }, - "sources": [ - "boe" - ], - "aliases": [ - "boe.bank_rate" - ], - "rid_patterns": [ - "boe.bank_rate.after_mpc_june_2026.first_print" - ], - "first_observed_period": "2026-06", - "last_observed_period": "2026-06", - "observation_count": 1 - }, - { - "uuid": "e762229a-d846-4e3e-a5f2-ec4361a675b6", - "concept": "boj.policy_rate_guideline.after_june_2026", - "family_patterns": [ - "boj.policy_rate_guideline.after_june_2026" - ], - "status": "observed", - "unit": "percent", - "cadence": "month", - "geography": { "id": "JP", - "level": "country", + "vintage": "current", "name": "Japan" }, "entity": { @@ -1507,17 +1672,18 @@ "boj" ], "aliases": [ - "boj.guideline_uncollateralized_overnight_call_rate" + "boj.guideline_uncollateralized_overnight_call_rate", + "boj.policy_rate_guideline.after_june_2026" ], "rid_patterns": [ - "boj.policy_rate_guideline.after_june_2026" + "boj.policy_rate_guideline.{P}" ], "first_observed_period": "2026-06", "last_observed_period": "2026-06", "observation_count": 1 }, { - "uuid": "4b79edf3-c0c3-4e69-afb8-dc5ca9ddda28", + "uuid": "aa0f4904-a802-4a63-afac-d259f650abe5", "concept": "census.construction_spending.total_mom", "family_patterns": [ "census.construction_spending.total_mom" @@ -1535,7 +1701,7 @@ "observation_count": 0 }, { - "uuid": "0fb9aba6-6d7b-4c8e-8c94-9583717b20c0", + "uuid": "8bc25a3b-fc09-4545-a543-fa2ec8e2f824", "concept": "census.housing.completions_saar", "family_patterns": [ "census.housing.completions_saar" @@ -1553,7 +1719,7 @@ "observation_count": 0 }, { - "uuid": "8a7fe6b2-c0bc-4714-b363-44fb2260b08d", + "uuid": "d2bf0b63-8c7b-415a-8bfc-f971ad464899", "concept": "census.housing.permits_saar", "family_patterns": [ "census.housing.permits_saar" @@ -1571,7 +1737,7 @@ "observation_count": 0 }, { - "uuid": "e4766a52-8647-4e82-b9fa-de3cf3e82edb", + "uuid": "f8fae70e-07e5-455c-8e2c-b0cf15cd99d1", "concept": "census.housing_starts.saar", "family_patterns": [ "census.housing_starts.saar.{P}" @@ -1580,8 +1746,9 @@ "unit": "millions", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -1589,51 +1756,64 @@ "role": "housing_start" }, "sources": [ - "census", - "census_housing" + "census" ], "aliases": [ - "HOUST", - "census.housing_starts.saar.2026-06", "census.housing_starts.saar.may_2026" ], "rid_patterns": [ "census.housing_starts.saar.{P}.first_print" ], "first_observed_period": "2026-05", - "last_observed_period": "2026-06", - "observation_count": 2 + "last_observed_period": "2026-05", + "observation_count": 1 }, { - "uuid": "aa40d039-021c-456b-bd74-7cd9cca7f974", - "concept": "census.m3.durable_goods_new_orders_mom", + "uuid": "e25e92f0-27d7-4de0-a096-bf745df47f0b", + "concept": "census.housing_starts.saar", "family_patterns": [ - "census.m3.durable_goods_new_orders_mom" + "census.housing_starts.saar.{P}" ], - "status": "docket-only", - "unit": "percent_growth", + "status": "observed", + "unit": "millions", "cadence": "month", - "geography": null, - "entity": null, - "sources": [], - "aliases": [], - "rid_patterns": [], - "first_observed_period": null, - "last_observed_period": null, - "observation_count": 0 + "geography": { + "level": "country", + "id": "0100000US", + "vintage": "current", + "name": "United States" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "census_housing" + ], + "aliases": [ + "HOUST", + "census.housing_starts.saar.2026-06" + ], + "rid_patterns": [ + "census.housing_starts.saar.{P}.first_print" + ], + "first_observed_period": "2026-06", + "last_observed_period": "2026-06", + "observation_count": 1 }, { - "uuid": "23759735-fa48-4726-8f62-2a8e7ffcf818", - "concept": "census.m3.durable_goods_new_orders_mom.2026_06", + "uuid": "3af7cdbd-2a38-4dca-8ff4-edadfa79de20", + "concept": "census.m3.durable_goods_new_orders_mom", "family_patterns": [ - "census.m3.durable_goods_new_orders_mom.2026_06" + "census.m3.durable_goods_new_orders_mom.{P}" ], "status": "observed", "unit": "percent_growth", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -1644,45 +1824,29 @@ "census_m3" ], "aliases": [ - "DGORDER" + "DGORDER", + "census.m3.durable_goods_new_orders_mom.2026_06" ], "rid_patterns": [ - "census.m3.durable_goods_new_orders_mom.2026_06.first_print" + "census.m3.durable_goods_new_orders_mom.{P}.first_print" ], "first_observed_period": "2026-06", "last_observed_period": "2026-06", "observation_count": 1 }, { - "uuid": "cd4bcf51-3810-4c14-a003-63160c6a68ae", + "uuid": "8112793f-5172-4cb5-83f1-afd909626624", "concept": "census.m3.durable_goods_shipments_mom", "family_patterns": [ - "census.m3.durable_goods_shipments_mom" - ], - "status": "docket-only", - "unit": "percent_growth", - "cadence": "month", - "geography": null, - "entity": null, - "sources": [], - "aliases": [], - "rid_patterns": [], - "first_observed_period": null, - "last_observed_period": null, - "observation_count": 0 - }, - { - "uuid": "fe9a6f40-a6a8-47a9-837d-f9bf6b9ddf18", - "concept": "census.m3.durable_goods_shipments_mom.2026_06", - "family_patterns": [ - "census.m3.durable_goods_shipments_mom.2026_06" + "census.m3.durable_goods_shipments_mom.{P}" ], "status": "observed", "unit": "percent_growth", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -1693,17 +1857,18 @@ "census_m3" ], "aliases": [ - "AMDMVS" + "AMDMVS", + "census.m3.durable_goods_shipments_mom.2026_06" ], "rid_patterns": [ - "census.m3.durable_goods_shipments_mom.2026_06.first_print" + "census.m3.durable_goods_shipments_mom.{P}.first_print" ], "first_observed_period": "2026-06", "last_observed_period": "2026-06", "observation_count": 1 }, { - "uuid": "cfc55af5-9d5e-41df-b743-abcdb6fefed0", + "uuid": "7c15316d-632c-40da-9c61-25b92a7531aa", "concept": "census.marts.adv44x72.monthly_change", "family_patterns": [ "census.marts.adv44x72.{P}.monthly_change" @@ -1712,8 +1877,9 @@ "unit": "percent_growth", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -1735,7 +1901,7 @@ "observation_count": 1 }, { - "uuid": "6f893276-193d-4f05-b97e-06f148c7e8b7", + "uuid": "686e3197-53ec-4988-ba8f-23c2bb90a902", "concept": "census.mtis.total_business_inventories_level", "family_patterns": [ "census.mtis.total_business_inventories_level.{P}" @@ -1744,8 +1910,9 @@ "unit": "usd_billions", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -1767,7 +1934,7 @@ "observation_count": 1 }, { - "uuid": "d068627e-4231-4112-99a2-585da493d712", + "uuid": "956247d0-69dd-4bde-859f-28b13905d719", "concept": "census.new_residential_sales.new_single_family_houses_sold_saar", "family_patterns": [ "census.new_residential_sales.new_single_family_houses_sold_saar" @@ -1785,7 +1952,7 @@ "observation_count": 0 }, { - "uuid": "64eecb42-6eaf-4d73-a0f4-2e780a381f25", + "uuid": "b69629ad-dd6a-4bfe-96b6-f92b99017d4d", "concept": "cms.care_compare.nursing_home_occupancy_pct", "family_patterns": [ "cms.care_compare.nursing_home_occupancy_pct.{P}" @@ -1794,8 +1961,9 @@ "unit": "percent", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -1817,7 +1985,7 @@ "observation_count": 1 }, { - "uuid": "06eaae11-5aa5-47e1-8d6d-281fa61014db", + "uuid": "24d9d68f-2ebc-44cf-b825-cb42f12d1103", "concept": "cms.medicaid_pi.beneficiaries_disenrolled_procedural", "family_patterns": [ "cms.medicaid_pi.beneficiaries_disenrolled_procedural" @@ -1826,8 +1994,9 @@ "unit": "count", "cadence": "month", "geography": { - "id": "0400000US06", "level": "state", + "id": "0400000US06", + "vintage": "current", "name": "California" }, "entity": { @@ -1841,14 +2010,14 @@ "Beneficiaries Disenrolled for Procedural Reasons at Renewal" ], "rid_patterns": [ - "cms.medicaid_pi.beneficiaries_disenrolled_procedural.california.feb_2026.original_submission" + "cms.medicaid_pi.beneficiaries_disenrolled_procedural.california.{P}.original_submission" ], "first_observed_period": "2026-02", "last_observed_period": "2026-02", "observation_count": 1 }, { - "uuid": "3b0fe92c-cc24-4b5c-8254-d8e594a4f0b1", + "uuid": "10692e21-bd74-47a7-a730-817da7aaec02", "concept": "cms.medicaid_pi.beneficiaries_disenrolled_total", "family_patterns": [ "cms.medicaid_pi.beneficiaries_disenrolled_total" @@ -1857,8 +2026,9 @@ "unit": "count", "cadence": "month", "geography": { - "id": "0400000US06", "level": "state", + "id": "0400000US06", + "vintage": "current", "name": "California" }, "entity": { @@ -1872,14 +2042,14 @@ "Beneficiaries Disenrolled at Renewal (Total)" ], "rid_patterns": [ - "cms.medicaid_pi.beneficiaries_disenrolled_total.california.feb_2026.original_submission" + "cms.medicaid_pi.beneficiaries_disenrolled_total.california.{P}.original_submission" ], "first_observed_period": "2026-02", "last_observed_period": "2026-02", "observation_count": 1 }, { - "uuid": "51cd0196-3b55-4fd3-a0f6-3b8df633ca5c", + "uuid": "24c9025c-770e-41b2-a701-6dd3fafa7822", "concept": "cms.medicaid_pi.beneficiaries_renewed_ex_parte", "family_patterns": [ "cms.medicaid_pi.beneficiaries_renewed_ex_parte" @@ -1888,8 +2058,9 @@ "unit": "count", "cadence": "month", "geography": { - "id": "0400000US06", "level": "state", + "id": "0400000US06", + "vintage": "current", "name": "California" }, "entity": { @@ -1903,14 +2074,14 @@ "Beneficiaries Whose Coverage Was Renewed on an Ex Parte Basis" ], "rid_patterns": [ - "cms.medicaid_pi.beneficiaries_renewed_ex_parte.california.feb_2026.original_submission" + "cms.medicaid_pi.beneficiaries_renewed_ex_parte.california.{P}.original_submission" ], "first_observed_period": "2026-02", "last_observed_period": "2026-02", "observation_count": 1 }, { - "uuid": "b60b0791-ac85-48e7-9823-f50374906eff", + "uuid": "ffe51507-ce80-4750-825d-087aeb8797de", "concept": "cms.medicaid_pi.beneficiaries_renewed_total", "family_patterns": [ "cms.medicaid_pi.beneficiaries_renewed_total" @@ -1919,8 +2090,9 @@ "unit": "count", "cadence": "month", "geography": { - "id": "0400000US06", "level": "state", + "id": "0400000US06", + "vintage": "current", "name": "California" }, "entity": { @@ -1934,14 +2106,14 @@ "Beneficiaries Whose Coverage Was Renewed (Total)" ], "rid_patterns": [ - "cms.medicaid_pi.beneficiaries_renewed_total.california.feb_2026.original_submission" + "cms.medicaid_pi.beneficiaries_renewed_total.california.{P}.original_submission" ], "first_observed_period": "2026-02", "last_observed_period": "2026-02", "observation_count": 1 }, { - "uuid": "d5cc5546-e4e9-4a65-a101-4cbbdeb836c6", + "uuid": "fbff6bd3-f4fc-42ae-b4d7-d39b9abf4f42", "concept": "cms.nursing_home_compare.reported_total_nurse_staffing_hprd_us", "family_patterns": [ "cms.nursing_home_compare.reported_total_nurse_staffing_hprd_us.{P}" @@ -1950,8 +2122,9 @@ "unit": "ratio", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -1973,7 +2146,7 @@ "observation_count": 1 }, { - "uuid": "b19d6746-752e-4f4a-a61b-6692ce739e56", + "uuid": "34291f59-cfc7-40ed-bf55-ab923b184b9a", "concept": "dol.eta.continued_claims.sa", "family_patterns": [ "dol.eta.continued_claims.sa" @@ -1982,8 +2155,9 @@ "unit": "millions", "cadence": "week_ending", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -2004,17 +2178,18 @@ "observation_count": 4 }, { - "uuid": "6c2ea998-62d0-4940-9c61-3025526f0f01", - "concept": "dol.eta.initial_claims.sa.week_ending_2026_06_06", + "uuid": "b77d7372-5127-4071-b927-e3f9452edc82", + "concept": "dol.eta.initial_claims.sa", "family_patterns": [ - "dol.eta.initial_claims.sa.week_ending_2026_06_06" + "dol.eta.initial_claims.sa.{P}" ], "status": "observed", "unit": "thousands", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -2024,26 +2199,29 @@ "sources": [ "dol" ], - "aliases": [], - "rid_patterns": [ + "aliases": [ "dol.eta.initial_claims.sa.week_ending_2026_06_06" ], + "rid_patterns": [ + "dol.eta.initial_claims.sa.{P}" + ], "first_observed_period": "2026-06", "last_observed_period": "2026-06", "observation_count": 1 }, { - "uuid": "a3b6da69-1b25-436f-b7d8-50402da8acd9", - "concept": "ecb.deposit_facility_rate.after_june_2026", + "uuid": "92d57201-4c72-4f51-90d5-d77a910662d0", + "concept": "ecb.deposit_facility_rate", "family_patterns": [ - "ecb.deposit_facility_rate.after_june_2026" + "ecb.deposit_facility_rate.{P}" ], "status": "observed", "unit": "percent", "cadence": "month", "geography": { - "id": "EA", "level": "country", + "id": "EA", + "vintage": "current", "name": "Euro area" }, "entity": { @@ -2053,26 +2231,29 @@ "sources": [ "ecb" ], - "aliases": [], - "rid_patterns": [ + "aliases": [ "ecb.deposit_facility_rate.after_june_2026" ], + "rid_patterns": [ + "ecb.deposit_facility_rate.{P}" + ], "first_observed_period": "2026-06", "last_observed_period": "2026-06", "observation_count": 1 }, { - "uuid": "5c60f36b-cff1-48ac-bea6-4d530a3a5b7b", - "concept": "estat.jp.cpi.core_exfreshfood.yoy.2026_05", + "uuid": "1b70b9e5-c422-4a91-ac1d-660012f09c12", + "concept": "estat.jp.cpi.core_exfreshfood.yoy", "family_patterns": [ - "estat.jp.cpi.core_exfreshfood.yoy.2026_05" + "estat.jp.cpi.core_exfreshfood.yoy.{P}" ], "status": "observed", "unit": "percent", "cadence": "month", "geography": { - "id": "JP", "level": "country", + "id": "JP", + "vintage": "current", "name": "Japan" }, "entity": { @@ -2083,6 +2264,7 @@ "statjp" ], "aliases": [ + "estat.jp.cpi.core_exfreshfood.yoy.2026_05", "japan.cpi.all_items_less_fresh_food_yoy" ], "rid_patterns": [ @@ -2093,7 +2275,7 @@ "observation_count": 1 }, { - "uuid": "665e37f8-73b3-4c18-9b6d-c59287b8516a", + "uuid": "d69ffa04-6d46-4f4e-b9c9-da2c4e9bfbdd", "concept": "eurostat.ea.hicp.flash.yoy", "family_patterns": [ "eurostat.ea.hicp.flash.yoy", @@ -2103,8 +2285,9 @@ "unit": "percent", "cadence": "month", "geography": { - "id": "EA21", "level": "region", + "id": "EA21", + "vintage": "current", "name": "Euro area" }, "entity": { @@ -2126,7 +2309,7 @@ "observation_count": 2 }, { - "uuid": "93bcc0bf-88fb-43df-8ec5-e388a2bf41dd", + "uuid": "e6dd2d67-f47a-4014-b591-a8fdb55f6cc7", "concept": "eurostat.hicp.all_items_annual_rate.euro_area", "family_patterns": [ "eurostat.hicp.all_items_annual_rate.euro_area.{P}" @@ -2135,8 +2318,9 @@ "unit": "percent", "cadence": "month", "geography": { - "id": "EA", "level": "region", + "id": "EA", + "vintage": "current", "name": "Euro area" }, "entity": { @@ -2146,21 +2330,51 @@ "sources": [ "eurostat" ], + "aliases": [ + "eurostat.hicp.all_items_annual_rate.euro_area.may_2026" + ], + "rid_patterns": [ + "eurostat.hicp.all_items_annual_rate.euro_area.{P}.final_first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "0a6b3f6e-c324-4eb1-9561-218ed26cb2bc", + "concept": "eurostat.hicp.all_items_annual_rate.euro_area", + "family_patterns": [ + "eurostat.hicp.all_items_annual_rate.euro_area.{P}" + ], + "status": "observed", + "unit": "percent", + "cadence": "month", + "geography": { + "level": "region", + "id": "EA21", + "vintage": "current", + "name": "Euro area" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "eurostat" + ], "aliases": [ "eurostat.hicp.all_items_annual_rate.euro_area.june_2026", - "eurostat.hicp.all_items_annual_rate.euro_area.may_2026", "prc_hicp_minr/M.RCH_A.TOTAL.EA21" ], "rid_patterns": [ - "eurostat.hicp.all_items_annual_rate.euro_area.{P}.final_first_print", "eurostat.hicp.all_items_annual_rate.euro_area.{P}.flash" ], - "first_observed_period": "2026-05", + "first_observed_period": "2026-06", "last_observed_period": "2026-06", - "observation_count": 2 + "observation_count": 1 }, { - "uuid": "575f6923-d16a-499a-9cf5-808ff49da510", + "uuid": "de81f2f8-dc4f-47ed-b94d-fea62024fc71", "concept": "eurostat.hicp.flash.yoy", "family_patterns": [ "eurostat.hicp.flash.yoy" @@ -2178,7 +2392,7 @@ "observation_count": 0 }, { - "uuid": "a75da4ba-3a7c-4b9e-92ee-9417d2d09947", + "uuid": "51bda8a2-7a5e-4643-87db-b66643485b8e", "concept": "eurostat.industrial_production.euro_area", "family_patterns": [ "eurostat.industrial_production.euro_area.{P}" @@ -2187,8 +2401,9 @@ "unit": "percent_growth", "cadence": "month", "geography": { - "id": "EA", "level": "region", + "id": "EA", + "vintage": "current", "name": "Euro area" }, "entity": { @@ -2209,7 +2424,7 @@ "observation_count": 1 }, { - "uuid": "c1cd6611-a941-4f6d-99e0-937b0cbe5923", + "uuid": "704fd1b0-0fa1-415f-868b-cf2c2f57178c", "concept": "eurostat.retail_trade.volume_mom.euro_area", "family_patterns": [ "eurostat.retail_trade.volume_mom.euro_area.{P}" @@ -2218,8 +2433,9 @@ "unit": "percent_growth", "cadence": "month", "geography": { - "id": "EA21", "level": "region", + "id": "EA21", + "vintage": "current", "name": "Euro area" }, "entity": { @@ -2241,7 +2457,7 @@ "observation_count": 1 }, { - "uuid": "f17d2b21-6462-4ffb-bb0d-0f9d9dfc1417", + "uuid": "72f86cdc-c980-4dcd-95ef-7970eae2658f", "concept": "eurostat.unemployment_rate", "family_patterns": [ "eurostat.unemployment_rate" @@ -2259,7 +2475,7 @@ "observation_count": 0 }, { - "uuid": "b52d84dc-c03a-4560-94ca-518486dbf83b", + "uuid": "14c8f455-9bb9-49aa-8ba2-83aaa7c42da8", "concept": "eurostat.unemployment_rate.belgium", "family_patterns": [ "eurostat.unemployment_rate.belgium" @@ -2267,7 +2483,11 @@ "status": "docket-only", "unit": null, "cadence": "month", - "geography": null, + "geography": { + "level": "country", + "id": "BE", + "name": null + }, "entity": null, "sources": [], "aliases": [], @@ -2277,7 +2497,7 @@ "observation_count": 0 }, { - "uuid": "1950ae76-e1ba-4795-aafb-d8dafeddb9d6", + "uuid": "be52503e-8272-4f00-b0d7-77b97501cf8f", "concept": "eurostat.unemployment_rate.euro_area", "family_patterns": [ "eurostat.unemployment_rate.euro_area.{P}" @@ -2286,8 +2506,9 @@ "unit": "percent", "cadence": "month", "geography": { - "id": "EA21", "level": "region", + "id": "EA21", + "vintage": "current", "name": "Euro area" }, "entity": { @@ -2309,7 +2530,7 @@ "observation_count": 1 }, { - "uuid": "adf2cf96-a222-4605-ad9f-e4d3a3f0a8bd", + "uuid": "08765b77-5015-4965-a990-6928f36d4994", "concept": "fed.g17.capacity_utilization.manufacturing", "family_patterns": [ "fed.g17.capacity_utilization.manufacturing" @@ -2327,7 +2548,7 @@ "observation_count": 0 }, { - "uuid": "0dde114c-ff49-4eef-a502-3f4f2c41ab22", + "uuid": "e535db92-cb5a-4767-8571-5d1c6fb507d3", "concept": "fed.g17.capacity_utilization.total_industry", "family_patterns": [ "fed.g17.capacity_utilization.total_industry.{P}" @@ -2336,8 +2557,42 @@ "unit": "percent", "cadence": "month", "geography": { + "level": "country", "id": "0100000US", + "vintage": "current", + "name": "United States" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "federal_reserve_g17" + ], + "aliases": [ + "TCU", + "fed.g17.capacity_utilization.total_industry.2026-06" + ], + "rid_patterns": [ + "fed.g17.capacity_utilization.total_industry.{P}.first_print" + ], + "first_observed_period": "2026-06", + "last_observed_period": "2026-06", + "observation_count": 1 + }, + { + "uuid": "f7a68fda-aebc-4b59-99b8-5bd30054a265", + "concept": "fed.g17.capacity_utilization.total_industry", + "family_patterns": [ + "fed.g17.capacity_utilization.total_industry.{P}" + ], + "status": "observed", + "unit": "percent", + "cadence": "month", + "geography": { "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -2345,23 +2600,20 @@ "role": "total_industry_capacity" }, "sources": [ - "fed", - "federal_reserve_g17" + "fed" ], "aliases": [ - "TCU", - "fed.g17.capacity_utilization.total_industry.2026-06", "fed.g17.capacity_utilization.total_industry.may_2026" ], "rid_patterns": [ "fed.g17.capacity_utilization.total_industry.{P}.first_print" ], "first_observed_period": "2026-05", - "last_observed_period": "2026-06", - "observation_count": 2 + "last_observed_period": "2026-05", + "observation_count": 1 }, { - "uuid": "9c1a0167-ae6d-4e37-86b3-b80829dbdd47", + "uuid": "84062e30-fb48-4ca2-8ade-d85a48f60116", "concept": "fed.g17.industrial_production.total_index_mom", "family_patterns": [ "fed.g17.industrial_production.total_index_mom.{P}" @@ -2370,8 +2622,42 @@ "unit": "percent_growth", "cadence": "month", "geography": { + "level": "country", "id": "0100000US", + "vintage": "current", + "name": "United States" + }, + "entity": { + "name": "economy", + "role": "aggregate" + }, + "sources": [ + "federal_reserve_g17" + ], + "aliases": [ + "INDPRO", + "fed.g17.industrial_production.total_index_mom.2026-06" + ], + "rid_patterns": [ + "fed.g17.industrial_production.total_index_mom.{P}.first_print" + ], + "first_observed_period": "2026-06", + "last_observed_period": "2026-06", + "observation_count": 1 + }, + { + "uuid": "9cd54337-5f6e-4235-ba3b-1b84ec98c1df", + "concept": "fed.g17.industrial_production.total_index_mom", + "family_patterns": [ + "fed.g17.industrial_production.total_index_mom.{P}" + ], + "status": "observed", + "unit": "percent_growth", + "cadence": "month", + "geography": { "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -2379,23 +2665,20 @@ "role": "total_industrial_production" }, "sources": [ - "fed", - "federal_reserve_g17" + "fed" ], "aliases": [ - "INDPRO", - "fed.g17.industrial_production.total_index_mom.2026-06", "fed.g17.industrial_production.total_index_mom.may_2026" ], "rid_patterns": [ "fed.g17.industrial_production.total_index_mom.{P}.first_print" ], "first_observed_period": "2026-05", - "last_observed_period": "2026-06", - "observation_count": 2 + "last_observed_period": "2026-05", + "observation_count": 1 }, { - "uuid": "baadb403-25a3-4701-a5e1-835b302e62b5", + "uuid": "9bea1d3b-1f9c-42d1-b260-dc01907d59a5", "concept": "fed.g17.manufacturing_production_mom", "family_patterns": [ "fed.g17.manufacturing_production_mom" @@ -2413,7 +2696,7 @@ "observation_count": 0 }, { - "uuid": "377d0771-4f1d-44f7-b4bd-33b73b376fcd", + "uuid": "57d9986e-d045-4a1e-a92e-1ef6663935cf", "concept": "fed.g19.consumer_credit_nonrevolving_annual_rate", "family_patterns": [ "fed.g19.consumer_credit_nonrevolving_annual_rate" @@ -2431,7 +2714,7 @@ "observation_count": 0 }, { - "uuid": "51bc97ca-1a1b-4e17-ac31-4fa260131484", + "uuid": "7bc4723e-f169-44e2-a5b5-e7f3d898ff65", "concept": "fed.g19.consumer_credit_revolving_annual_rate", "family_patterns": [ "fed.g19.consumer_credit_revolving_annual_rate" @@ -2443,71 +2726,1636 @@ "entity": null, "sources": [], "aliases": [], - "rid_patterns": [], - "first_observed_period": null, - "last_observed_period": null, - "observation_count": 0 + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "ca1e40a0-25be-4e48-87c3-f7cad75d61cb", + "concept": "fed.g19.consumer_credit_total_annual_rate", + "family_patterns": [ + "fed.g19.consumer_credit_total_annual_rate" + ], + "status": "docket-only", + "unit": "percent_growth", + "cadence": "month", + "geography": null, + "entity": null, + "sources": [], + "aliases": [], + "rid_patterns": [], + "first_observed_period": null, + "last_observed_period": null, + "observation_count": 0 + }, + { + "uuid": "0a1fc3c0-f785-4de8-a11d-aa41a4723dd4", + "concept": "fns.snap.application_processing_timeliness_rate", + "family_patterns": [ + "fns.snap.application_processing_timeliness_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US06", + "vintage": "current", + "name": "California" + }, + "entity": { + "name": "household", + "role": "snap_applicant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.application_processing_timeliness.california.{P}.official_release" + ], + "first_observed_period": "2024", + "last_observed_period": "2024", + "observation_count": 1 + }, + { + "uuid": "842e02b6-e337-498a-92f0-b93846f1e3f8", + "concept": "fns.snap.overpayment_error_rate", + "family_patterns": [ + "fns.snap.overpayment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "country", + "id": "0100000US", + "vintage": "current", + "name": "United States" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.overpayment_payment_error_rate.us.{P}.official_release" + ], + "first_observed_period": "2024", + "last_observed_period": "2024", + "observation_count": 1 + }, + { + "uuid": "494823b3-1b77-47fb-b32a-ddfabc80823d", + "concept": "fns.snap.share_jurisdictions_at_or_above_6pct", + "family_patterns": [ + "fns.snap.share_jurisdictions_at_or_above_6pct" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "country", + "id": "0100000US", + "vintage": "current", + "name": "United States" + }, + "entity": { + "name": "government", + "role": "snap_administering_jurisdiction" + }, + "sources": [ + "fns" + ], + "aliases": [ + "fns.snap.total_payment_error_rate" + ], + "rid_patterns": [ + "fns.snap.share_jurisdictions_at_or_above_6pct.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "c585a927-4655-47a3-bde5-f84910a67b04", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "country", + "id": "0100000US", + "vintage": "current", + "name": "United States" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.us.{P}", + "fns.snap.total_payment_error_rate.us.{P}.official_release" + ], + "first_observed_period": "2024", + "last_observed_period": "2025", + "observation_count": 2 + }, + { + "uuid": "ddb40496-e5be-45d0-96fc-554d5f8a34a3", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US01", + "vintage": "current", + "name": "Alabama" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.al.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "02726f8d-c850-4246-b4fa-097f875f373f", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US02", + "vintage": "current", + "name": "Alaska" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.ak.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "260f07f5-f86e-4c9f-b1b1-3e8c1eee0541", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US04", + "vintage": "current", + "name": "Arizona" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.az.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "0427fc19-e6a4-4aae-838a-92b97a98372f", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US05", + "vintage": "current", + "name": "Arkansas" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.ar.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "0e7e38c9-86e8-46f0-94dd-96306f6d7493", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US06", + "vintage": "current", + "name": "California" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.ca.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "6f729ed5-8c47-47e5-8665-296491ac9860", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US08", + "vintage": "current", + "name": "Colorado" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.co.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "2270ead6-e4ca-4562-9401-7e2a90fb12c7", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US09", + "vintage": "current", + "name": "Connecticut" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.ct.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "e15826d8-2e05-4e31-ad4b-0dafd26378eb", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US10", + "vintage": "current", + "name": "Delaware" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.de.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "2783a60d-fa4d-4790-8d33-8c9e8d8cc90f", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US11", + "vintage": "current", + "name": "District of Columbia" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.dc.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "f0a1bac8-592d-4c9b-ac1f-fe9c1625f025", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US12", + "vintage": "current", + "name": "Florida" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.fl.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "0eebf0eb-0a9e-4807-8eba-db432a0f7c53", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US13", + "vintage": "current", + "name": "Georgia" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.ga.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "519c4f40-16e7-4a05-8264-98ed96cfb991", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US15", + "vintage": "current", + "name": "Hawaii" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.hi.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "06abba46-1ae9-4f1d-a2ff-d44eace85eee", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US16", + "vintage": "current", + "name": "Idaho" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.id.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "8f5321ad-8b7f-47e5-8f3e-910bbaf8f893", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US17", + "vintage": "current", + "name": "Illinois" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.il.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "205d818e-5803-46e7-bd76-f46fb579dbd2", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US18", + "vintage": "current", + "name": "Indiana" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.in.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "5d57bcc2-286e-4545-bbbd-9c8c04fde6ae", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US19", + "vintage": "current", + "name": "Iowa" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.ia.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "9db5f35a-070a-4a98-b180-978663367e75", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US20", + "vintage": "current", + "name": "Kansas" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.ks.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "9fdbde0b-db7f-4405-ae40-2a14108b8e88", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US21", + "vintage": "current", + "name": "Kentucky" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.ky.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "3316dfa0-edd9-4f5f-921b-bbf7cec86b3e", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US22", + "vintage": "current", + "name": "Louisiana" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.la.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "8cec1cee-8967-4833-9f92-6fe8320e186c", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US23", + "vintage": "current", + "name": "Maine" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.me.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "e4975798-4e8e-40f9-a00b-2aaf34bb9094", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US24", + "vintage": "current", + "name": "Maryland" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.md.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "5cb38459-b88c-4108-9eec-38cbc1f08abe", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US25", + "vintage": "current", + "name": "Massachusetts" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.ma.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "c34d47f1-1f5d-4fea-a5e4-6fe6b73d3aaa", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US26", + "vintage": "current", + "name": "Michigan" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.mi.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "e867140c-7a90-434b-916c-45dbed98b58f", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US27", + "vintage": "current", + "name": "Minnesota" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.mn.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "406d950c-02d9-4290-b048-bd77e08b2d4d", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US28", + "vintage": "current", + "name": "Mississippi" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.ms.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "2aaa7a6d-2a07-439c-be44-bc84c9cc94f0", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US29", + "vintage": "current", + "name": "Missouri" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.mo.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "f6136a80-22a1-4ea9-94b7-d5f845818da9", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US30", + "vintage": "current", + "name": "Montana" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.mt.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "e281c4d7-10f4-48c7-9ad4-8cbe6ac083df", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US31", + "vintage": "current", + "name": "Nebraska" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.ne.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "c7dd3197-99d8-48e0-a40c-e5a1fad5d6e7", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US32", + "vintage": "current", + "name": "Nevada" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.nv.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "167dc6a7-13d2-4a30-a374-d201bd27eeda", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US33", + "vintage": "current", + "name": "New Hampshire" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.nh.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "1e8abd87-aa84-4b07-ab72-2e3ca543df47", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US34", + "vintage": "current", + "name": "New Jersey" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.nj.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "6288100f-0dc0-486d-8c47-a5f59422468d", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US35", + "vintage": "current", + "name": "New Mexico" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.nm.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "8466fe18-e7f0-4649-9fb6-fbe5c679b330", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US36", + "vintage": "current", + "name": "New York" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.ny.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "f2464c58-e3df-4c6d-bd8d-2224397e71e1", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US37", + "vintage": "current", + "name": "North Carolina" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.nc.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "0cfce5bb-e258-4461-941b-1bd22b702a7a", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US38", + "vintage": "current", + "name": "North Dakota" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.nd.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "653fc709-1048-41aa-92f9-478af77a7c37", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US39", + "vintage": "current", + "name": "Ohio" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.oh.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "b7b9352f-2ec2-442b-8cdf-91d4bee18834", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US40", + "vintage": "current", + "name": "Oklahoma" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.ok.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "36bf955e-147b-4a45-9a05-013ea643a982", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US41", + "vintage": "current", + "name": "Oregon" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.or.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "2d590ffd-f6cc-4ded-a836-66e51c06846a", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US42", + "vintage": "current", + "name": "Pennsylvania" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.pa.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "93cd55b3-4ac4-40a5-a894-2414b57d0b63", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US44", + "vintage": "current", + "name": "Rhode Island" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.ri.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "dd1b0426-a9a7-4947-8ccb-d725d5f705af", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US45", + "vintage": "current", + "name": "South Carolina" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.sc.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "88f0affd-b9e9-4e25-a5a1-75c8231e1851", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US46", + "vintage": "current", + "name": "South Dakota" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.sd.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "e64ace78-5211-42c7-b398-febe15bd72a9", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US47", + "vintage": "current", + "name": "Tennessee" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.tn.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "883bf5c9-2f23-46d7-ad2f-1735bb07da20", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US48", + "vintage": "current", + "name": "Texas" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.tx.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "70bd86dd-db63-43b9-bd4d-d3897c323fe4", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US49", + "vintage": "current", + "name": "Utah" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.ut.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "b5b9c3d2-e8d5-4217-9f1d-b10b583cd296", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US50", + "vintage": "current", + "name": "Vermont" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.vt.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "de6c7aca-f3fd-4ff3-953f-da914f079995", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US51", + "vintage": "current", + "name": "Virginia" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.va.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 }, { - "uuid": "32760935-b8c0-4442-a7c2-356fd154aa95", - "concept": "fed.g19.consumer_credit_total_annual_rate", + "uuid": "1ee101ec-a6ce-4a5d-85f2-bd0cf3799333", + "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ - "fed.g19.consumer_credit_total_annual_rate" + "fns.snap.total_payment_error_rate" + ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US53", + "vintage": "current", + "name": "Washington" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" ], - "status": "docket-only", - "unit": "percent_growth", - "cadence": "month", - "geography": null, - "entity": null, - "sources": [], "aliases": [], - "rid_patterns": [], - "first_observed_period": null, - "last_observed_period": null, - "observation_count": 0 + "rid_patterns": [ + "fns.snap.total_payment_error_rate.wa.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 }, { - "uuid": "4f83c1d3-3f00-4477-9363-be0b04f2165e", - "concept": "fns.snap.application_processing_timeliness_rate", + "uuid": "f0303e9e-dfcb-4d55-95fa-0db98a6e8dab", + "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ - "fns.snap.application_processing_timeliness_rate" + "fns.snap.total_payment_error_rate" ], "status": "observed", "unit": "percent", "cadence": "fiscal_year", "geography": { - "id": "0400000US06", "level": "state", - "name": "California" + "id": "0400000US54", + "vintage": "current", + "name": "West Virginia" }, "entity": { "name": "household", - "role": "snap_applicant" + "role": "snap_participant" }, "sources": [ "fns" ], "aliases": [], "rid_patterns": [ - "fns.snap.application_processing_timeliness.california.{P}.official_release" + "fns.snap.total_payment_error_rate.wv.{P}" ], - "first_observed_period": "2024", - "last_observed_period": "2024", + "first_observed_period": "2025", + "last_observed_period": "2025", "observation_count": 1 }, { - "uuid": "f0935448-d5c3-403e-802f-bf7c92dd6c60", - "concept": "fns.snap.overpayment_error_rate", + "uuid": "8105b4c6-62ba-469a-bdd7-5478df1c8984", + "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ - "fns.snap.overpayment_error_rate" + "fns.snap.total_payment_error_rate" ], "status": "observed", "unit": "percent", "cadence": "fiscal_year", "geography": { - "id": "0100000US", - "level": "country", - "name": "United States" + "level": "state", + "id": "0400000US55", + "vintage": "current", + "name": "Wisconsin" }, "entity": { "name": "household", @@ -2518,45 +4366,74 @@ ], "aliases": [], "rid_patterns": [ - "fns.snap.overpayment_payment_error_rate.us.{P}.official_release" + "fns.snap.total_payment_error_rate.wi.{P}" ], - "first_observed_period": "2024", - "last_observed_period": "2024", + "first_observed_period": "2025", + "last_observed_period": "2025", "observation_count": 1 }, { - "uuid": "b89d6078-eb8e-4eb1-92e2-cc436e2093b1", - "concept": "fns.snap.share_jurisdictions_at_or_above_6pct", + "uuid": "04540192-c9e8-45ff-bf3b-bd2038e41635", + "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ - "fns.snap.share_jurisdictions_at_or_above_6pct" + "fns.snap.total_payment_error_rate" ], "status": "observed", "unit": "percent", "cadence": "fiscal_year", "geography": { - "id": "0100000US", - "level": "country", - "name": "United States" + "level": "state", + "id": "0400000US56", + "vintage": "current", + "name": "Wyoming" }, "entity": { - "name": "government", - "role": "snap_administering_jurisdiction" + "name": "household", + "role": "snap_participant" }, "sources": [ "fns" ], - "aliases": [ + "aliases": [], + "rid_patterns": [ + "fns.snap.total_payment_error_rate.wy.{P}" + ], + "first_observed_period": "2025", + "last_observed_period": "2025", + "observation_count": 1 + }, + { + "uuid": "10797cc6-e647-4cae-a16f-168869ac91cd", + "concept": "fns.snap.total_payment_error_rate", + "family_patterns": [ "fns.snap.total_payment_error_rate" ], + "status": "observed", + "unit": "percent", + "cadence": "fiscal_year", + "geography": { + "level": "state", + "id": "0400000US66", + "vintage": "current", + "name": "Guam" + }, + "entity": { + "name": "household", + "role": "snap_participant" + }, + "sources": [ + "fns" + ], + "aliases": [], "rid_patterns": [ - "fns.snap.share_jurisdictions_at_or_above_6pct.{P}" + "fns.snap.total_payment_error_rate.gu.{P}" ], "first_observed_period": "2025", "last_observed_period": "2025", "observation_count": 1 }, { - "uuid": "38261880-d350-48fe-ab00-09580ed93dd2", + "uuid": "a7bf6571-716a-4d8f-8f3b-070bd4a1b975", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -2565,9 +4442,10 @@ "unit": "percent", "cadence": "fiscal_year", "geography": { - "id": "0100000US", - "level": "country", - "name": "United States" + "level": "state", + "id": "0400000US78", + "vintage": "current", + "name": "Virgin Islands" }, "entity": { "name": "household", @@ -2578,68 +4456,14 @@ ], "aliases": [], "rid_patterns": [ - "fns.snap.total_payment_error_rate.ak.{P}", - "fns.snap.total_payment_error_rate.al.{P}", - "fns.snap.total_payment_error_rate.ar.{P}", - "fns.snap.total_payment_error_rate.az.{P}", - "fns.snap.total_payment_error_rate.ca.{P}", - "fns.snap.total_payment_error_rate.co.{P}", - "fns.snap.total_payment_error_rate.ct.{P}", - "fns.snap.total_payment_error_rate.dc.{P}", - "fns.snap.total_payment_error_rate.de.{P}", - "fns.snap.total_payment_error_rate.fl.{P}", - "fns.snap.total_payment_error_rate.ga.{P}", - "fns.snap.total_payment_error_rate.gu.{P}", - "fns.snap.total_payment_error_rate.hi.{P}", - "fns.snap.total_payment_error_rate.ia.{P}", - "fns.snap.total_payment_error_rate.id.{P}", - "fns.snap.total_payment_error_rate.il.{P}", - "fns.snap.total_payment_error_rate.in.{P}", - "fns.snap.total_payment_error_rate.ks.{P}", - "fns.snap.total_payment_error_rate.ky.{P}", - "fns.snap.total_payment_error_rate.la.{P}", - "fns.snap.total_payment_error_rate.ma.{P}", - "fns.snap.total_payment_error_rate.md.{P}", - "fns.snap.total_payment_error_rate.me.{P}", - "fns.snap.total_payment_error_rate.mi.{P}", - "fns.snap.total_payment_error_rate.mn.{P}", - "fns.snap.total_payment_error_rate.mo.{P}", - "fns.snap.total_payment_error_rate.ms.{P}", - "fns.snap.total_payment_error_rate.mt.{P}", - "fns.snap.total_payment_error_rate.nc.{P}", - "fns.snap.total_payment_error_rate.nd.{P}", - "fns.snap.total_payment_error_rate.ne.{P}", - "fns.snap.total_payment_error_rate.nh.{P}", - "fns.snap.total_payment_error_rate.nj.{P}", - "fns.snap.total_payment_error_rate.nm.{P}", - "fns.snap.total_payment_error_rate.nv.{P}", - "fns.snap.total_payment_error_rate.ny.{P}", - "fns.snap.total_payment_error_rate.oh.{P}", - "fns.snap.total_payment_error_rate.ok.{P}", - "fns.snap.total_payment_error_rate.or.{P}", - "fns.snap.total_payment_error_rate.pa.{P}", - "fns.snap.total_payment_error_rate.ri.{P}", - "fns.snap.total_payment_error_rate.sc.{P}", - "fns.snap.total_payment_error_rate.sd.{P}", - "fns.snap.total_payment_error_rate.tn.{P}", - "fns.snap.total_payment_error_rate.tx.{P}", - "fns.snap.total_payment_error_rate.us.{P}", - "fns.snap.total_payment_error_rate.us.{P}.official_release", - "fns.snap.total_payment_error_rate.ut.{P}", - "fns.snap.total_payment_error_rate.va.{P}", - "fns.snap.total_payment_error_rate.vi.{P}", - "fns.snap.total_payment_error_rate.vt.{P}", - "fns.snap.total_payment_error_rate.wa.{P}", - "fns.snap.total_payment_error_rate.wi.{P}", - "fns.snap.total_payment_error_rate.wv.{P}", - "fns.snap.total_payment_error_rate.wy.{P}" + "fns.snap.total_payment_error_rate.vi.{P}" ], - "first_observed_period": "2024", + "first_observed_period": "2025", "last_observed_period": "2025", - "observation_count": 55 + "observation_count": 1 }, { - "uuid": "2a8cd82e-c6a6-4374-b53f-361c8d912ad5", + "uuid": "e07881a2-39b4-416f-8cd9-c503b1fdad4a", "concept": "fns.snap.total_persons", "family_patterns": [ "fns.snap.total_persons" @@ -2657,7 +4481,7 @@ "observation_count": 0 }, { - "uuid": "d39d61fc-dfdc-4ac2-825e-fc2fae47206c", + "uuid": "d4710c2c-cdb9-49ff-aa96-3bfb53429dbb", "concept": "fns.snap.underpayment_error_rate", "family_patterns": [ "fns.snap.underpayment_error_rate" @@ -2666,8 +4490,9 @@ "unit": "percent", "cadence": "fiscal_year", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -2686,7 +4511,7 @@ "observation_count": 1 }, { - "uuid": "230d55c0-c136-4cea-870e-df157e7dd8a5", + "uuid": "37d08842-a1c9-40f3-b8dd-8fe010906a9d", "concept": "fns.wic.total_participation", "family_patterns": [ "fns.wic.total_participation" @@ -2704,7 +4529,7 @@ "observation_count": 0 }, { - "uuid": "cb766816-11af-41c0-ade0-44b8b80070c3", + "uuid": "10b01c7f-95e4-498d-a832-486ed61b1c51", "concept": "nbb.business_barometer.overall", "family_patterns": [ "nbb.business_barometer.overall" @@ -2712,7 +4537,11 @@ "status": "docket-only", "unit": "index_points", "cadence": "month", - "geography": null, + "geography": { + "level": "country", + "id": "BE", + "name": null + }, "entity": null, "sources": [], "aliases": [], @@ -2722,7 +4551,7 @@ "observation_count": 0 }, { - "uuid": "cc753c6f-3189-40bc-ac91-a3fc030bca77", + "uuid": "5e7f88da-8f50-4e0e-9b7b-913da0b9d53e", "concept": "nbb.consumer_confidence.indicator", "family_patterns": [ "nbb.consumer_confidence.indicator" @@ -2730,7 +4559,11 @@ "status": "docket-only", "unit": "index_points", "cadence": "month", - "geography": null, + "geography": { + "level": "country", + "id": "BE", + "name": null + }, "entity": null, "sources": [], "aliases": [], @@ -2740,7 +4573,7 @@ "observation_count": 0 }, { - "uuid": "d323b820-4286-4af8-abde-ffc23a6a5cf0", + "uuid": "bd245ca4-e8b7-45e5-b998-72601a3d75fb", "concept": "nbb.gdp.flash_qoq", "family_patterns": [ "nbb.gdp.flash_qoq" @@ -2748,7 +4581,11 @@ "status": "docket-only", "unit": null, "cadence": "quarter", - "geography": null, + "geography": { + "level": "country", + "id": "BE", + "name": null + }, "entity": null, "sources": [], "aliases": [], @@ -2758,7 +4595,7 @@ "observation_count": 0 }, { - "uuid": "5e675e85-d2c9-4752-ac42-12ffd7f2ab19", + "uuid": "9dc148f2-844f-438f-94a6-1ad7197c9fa1", "concept": "ons.cpi.annual_rate", "family_patterns": [ "ons.cpi.annual_rate.{P}" @@ -2767,8 +4604,9 @@ "unit": "percent", "cadence": "month", "geography": { - "id": "GB", "level": "country", + "id": "GB", + "vintage": "current", "name": "United Kingdom" }, "entity": { @@ -2789,17 +4627,18 @@ "observation_count": 1 }, { - "uuid": "03049148-7355-45b8-b84f-ea5e2ed68132", - "concept": "ons.cpih.annual_rate.2026_05", + "uuid": "bb244193-568a-465b-ae0b-bdae1fb23ae8", + "concept": "ons.cpih.annual_rate", "family_patterns": [ - "ons.cpih.annual_rate.2026_05" + "ons.cpih.annual_rate.{P}" ], "status": "observed", "unit": "percent", "cadence": "month", "geography": { - "id": "GB", "level": "country", + "id": "GB", + "vintage": "current", "name": "United Kingdom" }, "entity": { @@ -2810,7 +4649,7 @@ "ons" ], "aliases": [ - "ons.cpih.annual_rate" + "ons.cpih.annual_rate.2026_05" ], "rid_patterns": [ "ons.cpih.annual_rate.{P}" @@ -2820,7 +4659,7 @@ "observation_count": 1 }, { - "uuid": "f555c66e-3046-4937-868c-b177fb90349c", + "uuid": "0ccc9fc8-6ad9-44d6-a406-a13c0c6b779e", "concept": "ons.gdp.monthly_growth", "family_patterns": [ "ons.gdp.monthly_growth.{P}" @@ -2829,8 +4668,9 @@ "unit": "percent_growth", "cadence": "month", "geography": { - "id": "GB", "level": "country", + "id": "GB", + "vintage": "current", "name": "United Kingdom" }, "entity": { @@ -2851,7 +4691,7 @@ "observation_count": 1 }, { - "uuid": "a52fff34-0ed5-484f-8f9d-351bf58fdc28", + "uuid": "5394d93b-56a4-47fd-89e3-d0706df4ee7e", "concept": "ons.hmrc.paye_payrolled_employees", "family_patterns": [ "ons.hmrc.paye_payrolled_employees.{P}" @@ -2860,8 +4700,9 @@ "unit": "millions", "cadence": "month", "geography": { - "id": "GB", "level": "country", + "id": "GB", + "vintage": "current", "name": "United Kingdom" }, "entity": { @@ -2882,17 +4723,18 @@ "observation_count": 1 }, { - "uuid": "1c721376-4909-4a00-8720-20cfc7a974e0", - "concept": "ons.labour.unemployment_rate.february_to_april_2026", + "uuid": "e58098cf-6320-4028-9b00-9fdfb4a8f9dc", + "concept": "ons.labour.unemployment_rate", "family_patterns": [ - "ons.labour.unemployment_rate.february_to_april_2026" + "ons.labour.unemployment_rate.{P}" ], "status": "observed", "unit": "percent", "cadence": "month", "geography": { - "id": "GB", "level": "country", + "id": "GB", + "vintage": "current", "name": "United Kingdom" }, "entity": { @@ -2903,17 +4745,17 @@ "ons" ], "aliases": [ - "ons.labour.unemployment_rate" + "ons.labour.unemployment_rate.february_to_april_2026" ], "rid_patterns": [ - "ons.labour.unemployment_rate.february_to_april_2026.first_print" + "ons.labour.unemployment_rate.{P}.first_print" ], "first_observed_period": "2026-04", "last_observed_period": "2026-04", "observation_count": 1 }, { - "uuid": "5be5655b-d8c5-487b-80fa-56398c1a340b", + "uuid": "7c35b6d4-a697-40d8-9c66-b8809728b54d", "concept": "ons.pusf.j5ii.public_sector_net_borrowing_ex_banks", "family_patterns": [ "ons.pusf.j5ii.public_sector_net_borrowing_ex_banks.{P}" @@ -2922,8 +4764,9 @@ "unit": "gbp_billions", "cadence": "month", "geography": { - "id": "GB", "level": "country", + "id": "GB", + "vintage": "current", "name": "United Kingdom" }, "entity": { @@ -2945,7 +4788,7 @@ "observation_count": 1 }, { - "uuid": "56f25283-15e0-401e-9702-fab516d563bc", + "uuid": "27924b23-60c0-4a61-aadc-8a2a2889b4ec", "concept": "ons.retail_sales.volume_mom", "family_patterns": [ "ons.retail_sales.volume_mom.{P}" @@ -2954,8 +4797,9 @@ "unit": "percent_growth", "cadence": "month", "geography": { - "id": "GB", "level": "country", + "id": "GB", + "vintage": "current", "name": "Great Britain" }, "entity": { @@ -2976,17 +4820,18 @@ "observation_count": 1 }, { - "uuid": "8ef3fb76-ccf9-44cc-b893-45cc3789a2e5", - "concept": "rba.cash_rate_target.after_june_2026", + "uuid": "b0ba0014-50a8-46b0-9549-d51d03a281d3", + "concept": "rba.cash_rate_target", "family_patterns": [ - "rba.cash_rate_target.after_june_2026" + "rba.cash_rate_target.{P}" ], "status": "observed", "unit": "percent", "cadence": "month", "geography": { - "id": "AU", "level": "country", + "id": "AU", + "vintage": "current", "name": "Australia" }, "entity": { @@ -2997,17 +4842,17 @@ "rba" ], "aliases": [ - "rba.cash_rate_target" + "rba.cash_rate_target.after_june_2026" ], "rid_patterns": [ - "rba.cash_rate_target.after_june_2026" + "rba.cash_rate_target.{P}" ], "first_observed_period": "2026-06", "last_observed_period": "2026-06", "observation_count": 1 }, { - "uuid": "1a66b6f4-2a13-4e1b-a69c-5720e3e48cd9", + "uuid": "b311da3b-4e74-4c77-955c-b40673658591", "concept": "ssa.ssi.total_recipients", "family_patterns": [ "ssa.ssi.total_recipients" @@ -3025,7 +4870,7 @@ "observation_count": 0 }, { - "uuid": "65865379-9dcd-426f-bd52-537563d618b4", + "uuid": "9069a65e-39d1-4735-99f3-553e6e90be7a", "concept": "statbel.cpi.headline_yoy", "family_patterns": [ "statbel.cpi.headline_yoy" @@ -3033,7 +4878,11 @@ "status": "docket-only", "unit": null, "cadence": "month", - "geography": null, + "geography": { + "level": "country", + "id": "BE", + "name": null + }, "entity": null, "sources": [], "aliases": [], @@ -3043,7 +4892,7 @@ "observation_count": 0 }, { - "uuid": "ed77763d-9352-4f7c-bc54-2d85f12a436d", + "uuid": "4744d1f0-7fe3-4dcc-8b93-e8b46af4a0f7", "concept": "statbel.health_index.yoy", "family_patterns": [ "statbel.health_index.yoy" @@ -3051,7 +4900,11 @@ "status": "docket-only", "unit": null, "cadence": "month", - "geography": null, + "geography": { + "level": "country", + "id": "BE", + "name": null + }, "entity": null, "sources": [], "aliases": [], @@ -3061,7 +4914,7 @@ "observation_count": 0 }, { - "uuid": "611888ac-a911-4ba8-963d-5b06f8978970", + "uuid": "f1330368-0ba5-4649-94a3-50400bd50154", "concept": "statcan.36-10-0434-01.all_industries.month_to_month_percent_change", "family_patterns": [ "statcan.36-10-0434-01.all_industries.month_to_month_percent_change" @@ -3070,8 +4923,9 @@ "unit": "percent_growth", "cadence": "month", "geography": { - "id": "CA", "level": "country", + "id": "CA", + "vintage": "current", "name": "Canada" }, "entity": { @@ -3092,7 +4946,7 @@ "observation_count": 1 }, { - "uuid": "56932066-fd94-4d42-b8e7-06dbdd798031", + "uuid": "72f752bb-57a5-4556-875c-a6c14db3d0fa", "concept": "statcan.building_permits.total_value_mom.canada", "family_patterns": [ "statcan.building_permits.total_value_mom.canada.{P}" @@ -3101,8 +4955,9 @@ "unit": "percent_growth", "cadence": "month", "geography": { - "id": "CA", "level": "country", + "id": "CA", + "vintage": "current", "name": "Canada" }, "entity": { @@ -3123,7 +4978,7 @@ "observation_count": 1 }, { - "uuid": "79e69ae4-7585-47b5-a119-1bef1fe8fa5a", + "uuid": "1cc75c76-34d2-47d6-ac69-db9a5e75b16f", "concept": "statcan.cpi.all_items_annual_rate.canada", "family_patterns": [ "statcan.cpi.all_items_annual_rate.canada.{P}" @@ -3132,8 +4987,9 @@ "unit": "percent", "cadence": "month", "geography": { - "id": "CA", "level": "country", + "id": "CA", + "vintage": "current", "name": "Canada" }, "entity": { @@ -3155,7 +5011,7 @@ "observation_count": 1 }, { - "uuid": "9ef07249-8a6a-48c1-9a93-6a0170f4b2d9", + "uuid": "182061a6-d331-4649-83c8-a4252139247f", "concept": "statcan.cpi.allitems.yoy", "family_patterns": [ "statcan.cpi.allitems.yoy.{P}" @@ -3164,8 +5020,9 @@ "unit": "percent", "cadence": "month", "geography": { - "id": "CA", "level": "country", + "id": "CA", + "vintage": "current", "name": "Canada" }, "entity": { @@ -3187,7 +5044,7 @@ "observation_count": 1 }, { - "uuid": "bbe90a65-3a7e-4a55-b53f-a4bd026a71bb", + "uuid": "8fc52914-0d14-4d2f-a74b-5f5edfd9aeee", "concept": "statcan.employment_insurance.regular_beneficiaries.canada", "family_patterns": [ "statcan.employment_insurance.regular_beneficiaries.canada.{P}" @@ -3196,8 +5053,42 @@ "unit": "thousands", "cadence": "month", "geography": { + "level": "country", "id": "CA", + "vintage": "current", + "name": "Canada" + }, + "entity": { + "name": "person", + "role": "ei_beneficiary" + }, + "sources": [ + "statcan" + ], + "aliases": [ + "statcan.employment_insurance.regular_beneficiaries.canada.may_2026", + "v64549350" + ], + "rid_patterns": [ + "statcan.employment_insurance.regular_beneficiaries.canada.{P}.first_print" + ], + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", + "observation_count": 1 + }, + { + "uuid": "aaf2c099-57ef-4978-a0c1-5a3d93a355cf", + "concept": "statcan.employment_insurance.regular_beneficiaries.canada", + "family_patterns": [ + "statcan.employment_insurance.regular_beneficiaries.canada.{P}" + ], + "status": "observed", + "unit": "thousands", + "cadence": "month", + "geography": { "level": "country", + "id": "CA", + "vintage": "current", "name": "Canada" }, "entity": { @@ -3209,19 +5100,17 @@ ], "aliases": [ "statcan.employment_insurance.regular_beneficiaries", - "statcan.employment_insurance.regular_beneficiaries.canada.april_2026", - "statcan.employment_insurance.regular_beneficiaries.canada.may_2026", - "v64549350" + "statcan.employment_insurance.regular_beneficiaries.canada.april_2026" ], "rid_patterns": [ "statcan.employment_insurance.regular_beneficiaries.canada.{P}.first_print" ], "first_observed_period": "2026-04", - "last_observed_period": "2026-05", - "observation_count": 2 + "last_observed_period": "2026-04", + "observation_count": 1 }, { - "uuid": "79d82a2e-0005-413e-a27f-68a892cb9c92", + "uuid": "37a2108c-fc20-4773-8955-1fa673d4ea39", "concept": "statcan.gdp_by_industry.monthly_growth", "family_patterns": [ "statcan.gdp_by_industry.monthly_growth.{P}" @@ -3230,8 +5119,9 @@ "unit": "percent_growth", "cadence": "month", "geography": { - "id": "CA", "level": "country", + "id": "CA", + "vintage": "current", "name": "Canada" }, "entity": { @@ -3253,7 +5143,7 @@ "observation_count": 1 }, { - "uuid": "b54ca57d-6487-4449-8004-7654676c6d8c", + "uuid": "eefd1b57-4894-4b89-a207-854dd60f3a27", "concept": "statcan.lfs.employment_change", "family_patterns": [ "statcan.lfs.employment_change" @@ -3262,8 +5152,9 @@ "unit": "thousands", "cadence": "month", "geography": { - "id": "CA", "level": "country", + "id": "CA", + "vintage": "current", "name": "Canada" }, "entity": { @@ -3282,7 +5173,7 @@ "observation_count": 1 }, { - "uuid": "b6a06cd2-6961-442e-9549-e418a73ee8fb", + "uuid": "9c699030-1f40-4bd3-baca-b84a13ebf54e", "concept": "statcan.lfs.unemployment_rate", "family_patterns": [ "statcan.lfs.unemployment_rate" @@ -3291,8 +5182,9 @@ "unit": "percent", "cadence": "month", "geography": { - "id": "CA", "level": "country", + "id": "CA", + "vintage": "current", "name": "Canada" }, "entity": { @@ -3311,7 +5203,7 @@ "observation_count": 1 }, { - "uuid": "1bc3ad4b-a496-49be-a37f-2ecc68aa7f9e", + "uuid": "1404f983-f6b2-4f65-aa64-cfcbe00f3718", "concept": "statcan.retail_trade.sales_mom.canada", "family_patterns": [ "statcan.retail_trade.sales_mom.canada.{P}" @@ -3320,8 +5212,9 @@ "unit": "percent_growth", "cadence": "month", "geography": { - "id": "CA", "level": "country", + "id": "CA", + "vintage": "current", "name": "Canada" }, "entity": { @@ -3343,7 +5236,7 @@ "observation_count": 1 }, { - "uuid": "500d4013-11d3-4953-a655-f66a0a8e6c2e", + "uuid": "0c1046ce-6fd7-420b-93e2-77b9800ff91a", "concept": "statcan.wholesale_trade.sales_mom_exclusions.canada", "family_patterns": [ "statcan.wholesale_trade.sales_mom_exclusions.canada.{P}" @@ -3352,8 +5245,9 @@ "unit": "percent_growth", "cadence": "month", "geography": { - "id": "CA", "level": "country", + "id": "CA", + "vintage": "current", "name": "Canada" }, "entity": { @@ -3375,7 +5269,7 @@ "observation_count": 1 }, { - "uuid": "fe21f760-19bc-442d-8096-9ad6fd519da8", + "uuid": "ed14fae9-fe4c-4f4c-bd85-c4df6df4b05f", "concept": "statjp.cpi.all_items_annual_rate.japan", "family_patterns": [ "statjp.cpi.all_items_annual_rate.japan.{P}" @@ -3384,8 +5278,9 @@ "unit": "percent", "cadence": "month", "geography": { - "id": "JP", "level": "country", + "id": "JP", + "vintage": "current", "name": "Japan" }, "entity": { @@ -3407,7 +5302,7 @@ "observation_count": 1 }, { - "uuid": "82e6ff19-58d9-47de-8c9a-290d216b55d6", + "uuid": "fa8946a8-e52a-4a62-a75f-9facf4117d84", "concept": "statjp.cpi.tokyo_all_items_annual_rate", "family_patterns": [ "statjp.cpi.tokyo_all_items_annual_rate.{P}" @@ -3416,8 +5311,9 @@ "unit": "percent", "cadence": "month", "geography": { - "id": "JP", "level": "country", + "id": "JP", + "vintage": "current", "name": "Japan" }, "entity": { @@ -3439,7 +5335,7 @@ "observation_count": 1 }, { - "uuid": "3fdadece-0a8d-47ef-82e2-5fdd6c1635a3", + "uuid": "e2312b47-b581-445c-867c-71bd50123492", "concept": "statjp.cpi.tokyo_all_items_yoy", "family_patterns": [ "statjp.cpi.tokyo_all_items_yoy" @@ -3457,7 +5353,7 @@ "observation_count": 0 }, { - "uuid": "819db852-efc0-4328-b6f9-921812037422", + "uuid": "ec446a59-3ea0-4dc1-804b-b6a40506d6fa", "concept": "statjp.household_spending.real_yoy.two_or_more_person_households", "family_patterns": [ "statjp.household_spending.real_yoy.two_or_more_person_households.{P}" @@ -3466,8 +5362,9 @@ "unit": "percent_growth", "cadence": "month", "geography": { - "id": "JP", "level": "country", + "id": "JP", + "vintage": "current", "name": "Japan" }, "entity": { @@ -3489,7 +5386,7 @@ "observation_count": 1 }, { - "uuid": "872a5b54-68b8-43ac-ae8e-2ed34d1d6248", + "uuid": "09e40854-6b14-4128-9c54-719192ba9eae", "concept": "statjp.lfs.unemployment_rate.japan", "family_patterns": [ "statjp.lfs.unemployment_rate.japan.{P}" @@ -3498,8 +5395,9 @@ "unit": "percent", "cadence": "month", "geography": { - "id": "JP", "level": "country", + "id": "JP", + "vintage": "current", "name": "Japan" }, "entity": { @@ -3521,7 +5419,7 @@ "observation_count": 1 }, { - "uuid": "889ffc28-5927-45d6-b1bd-042cea0eebd9", + "uuid": "dc7d23ca-63ab-40f1-affa-213cf3880dde", "concept": "treasury.mts.monthly_deficit", "family_patterns": [ "treasury.mts.monthly_deficit.{P}" @@ -3530,8 +5428,9 @@ "unit": "usd_billions", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -3552,7 +5451,7 @@ "observation_count": 1 }, { - "uuid": "eef582df-ebc4-458d-bcb2-c577eec6f486", + "uuid": "d37c0017-e5d6-4b7e-afc6-3cc3322bec38", "concept": "us.bea.core_pce.mom_sa", "family_patterns": [ "us.bea.core_pce.mom_sa.{P}" @@ -3561,8 +5460,9 @@ "unit": "percent_growth", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -3585,17 +5485,18 @@ "observation_count": 2 }, { - "uuid": "9d57de07-64b7-466b-94db-fb5be3b6ad8b", - "concept": "us.census.housing_starts.total_saar.2026_05", + "uuid": "d085b526-5557-422f-b623-296d7152cbe0", + "concept": "us.census.housing_starts.total_saar", "family_patterns": [ - "us.census.housing_starts.total_saar.2026_05" + "us.census.housing_starts.total_saar.{P}" ], "status": "observed", "unit": "millions", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -3606,7 +5507,8 @@ "census" ], "aliases": [ - "census.housing_starts.saar" + "census.housing_starts.saar", + "us.census.housing_starts.total_saar.2026_05" ], "rid_patterns": [ "us.census.housing_starts.total_saar.{P}" @@ -3616,7 +5518,7 @@ "observation_count": 1 }, { - "uuid": "20531c77-574e-4a75-b088-efd30e8348de", + "uuid": "cf51edfa-cbc0-4d2d-ade1-32a1240da80f", "concept": "us.dol.initial_claims.sa", "family_patterns": [ "us.dol.initial_claims.sa" @@ -3625,8 +5527,9 @@ "unit": "thousands", "cadence": "week_ending", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -3647,17 +5550,18 @@ "observation_count": 5 }, { - "uuid": "ac51922e-1cd3-492e-a3bf-f1d9abe3086b", - "concept": "us.dol.initial_claims.sa.week_2026_06_13", + "uuid": "1b5eec21-e424-44fe-a922-d99929901585", + "concept": "us.dol.initial_claims.sa", "family_patterns": [ - "us.dol.initial_claims.sa.week_2026_06_13" + "us.dol.initial_claims.sa.{P}" ], "status": "observed", "unit": "thousands", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -3668,7 +5572,8 @@ "dol" ], "aliases": [ - "dol.eta.initial_claims.sa" + "dol.eta.initial_claims.sa", + "us.dol.initial_claims.sa.week_2026_06_13" ], "rid_patterns": [ "us.dol.initial_claims.sa.{P}" @@ -3678,17 +5583,18 @@ "observation_count": 1 }, { - "uuid": "4adf85b7-5252-4241-811f-c6f29c8b2521", - "concept": "us.fed.fomc.target_range_upper.2026_06", + "uuid": "5b0068e3-675a-4c77-90a7-1bcfdc69f11d", + "concept": "us.fed.fomc.target_range_upper", "family_patterns": [ - "us.fed.fomc.target_range_upper.2026_06" + "us.fed.fomc.target_range_upper.{P}" ], "status": "observed", "unit": "percent", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -3699,7 +5605,8 @@ "fed" ], "aliases": [ - "fomc.federal_funds_target_range_upper" + "fomc.federal_funds_target_range_upper", + "us.fed.fomc.target_range_upper.2026_06" ], "rid_patterns": [ "us.fed.fomc.target_range_upper.{P}" @@ -3709,17 +5616,18 @@ "observation_count": 1 }, { - "uuid": "4d47758b-a8f8-4dde-a2ef-b27b579c4b00", - "concept": "us.frb.industrial_production.total.mom_sa.2026_05", + "uuid": "f7b45e6f-5085-4bb0-9645-cf0f915f0cae", + "concept": "us.frb.industrial_production.total.mom_sa", "family_patterns": [ - "us.frb.industrial_production.total.mom_sa.2026_05" + "us.frb.industrial_production.total.mom_sa.{P}" ], "status": "observed", "unit": "percent_growth", "cadence": "month", "geography": { - "id": "0100000US", "level": "country", + "id": "0100000US", + "vintage": "current", "name": "United States" }, "entity": { @@ -3730,7 +5638,8 @@ "fed" ], "aliases": [ - "fed.g17.industrial_production.total_index_mom" + "fed.g17.industrial_production.total_index_mom", + "us.frb.industrial_production.total.mom_sa.2026_05" ], "rid_patterns": [ "us.frb.industrial_production.total.mom_sa.{P}" @@ -3740,7 +5649,7 @@ "observation_count": 1 }, { - "uuid": "d36768f5-d533-4550-aac7-8fe066495e99", + "uuid": "f0b3ebdd-f2b2-4688-b8b4-325bcd9485c9", "concept": "usaspending.dod.new_prime_awards", "family_patterns": [ "usaspending.dod.new_prime_awards" @@ -3758,7 +5667,7 @@ "observation_count": 0 }, { - "uuid": "66c6b363-bcfb-49c3-b2d7-419114d7808d", + "uuid": "b842fec2-c251-48a9-9aea-7ad64113ee55", "concept": "usaspending.dod.prime_award_obligations", "family_patterns": [ "usaspending.dod.prime_award_obligations" @@ -3776,7 +5685,7 @@ "observation_count": 0 }, { - "uuid": "ad1f0a62-2372-4426-9eff-8feaaccb2e01", + "uuid": "9154e0d2-eff3-4530-af75-b363d6d88809", "concept": "usaspending.dod.prime_award_transactions", "family_patterns": [ "usaspending.dod.prime_award_transactions" @@ -3794,7 +5703,7 @@ "observation_count": 0 }, { - "uuid": "e12a0711-78b7-4516-929c-1b26888cf4d3", + "uuid": "dde380c6-2c4f-402c-ae36-e1f6f8c4da2c", "concept": "usaspending.dod.prime_contract_obligations", "family_patterns": [ "usaspending.dod.prime_contract_obligations" @@ -3812,7 +5721,7 @@ "observation_count": 0 }, { - "uuid": "4c300bbd-097c-4449-854b-620f110c2e96", + "uuid": "f9cf8a2a-dddc-4086-8ee0-12d7aa97e6c6", "concept": "usaspending.dod.small_business_contract_obligation_share", "family_patterns": [ "usaspending.dod.small_business_contract_obligation_share" @@ -3830,7 +5739,7 @@ "observation_count": 0 }, { - "uuid": "4c11c9b0-20ab-4344-96c6-9e89d1b9f340", + "uuid": "e00e27d8-c584-4310-ab1a-3f2dce010041", "concept": "usaspending.dod.unique_prime_contract_recipients", "family_patterns": [ "usaspending.dod.unique_prime_contract_recipients" @@ -3848,7 +5757,7 @@ "observation_count": 0 }, { - "uuid": "4e30f398-c834-456f-846d-f73b155f29bc", + "uuid": "07209de1-43ea-4728-985b-948c9a6eb3e1", "concept": "usda.fsa.crp.enrolled_acres_total", "family_patterns": [ "usda.fsa.crp.enrolled_acres_total" @@ -3856,7 +5765,11 @@ "status": "docket-only", "unit": "count", "cadence": "month", - "geography": null, + "geography": { + "level": "country", + "id": "0100000US", + "name": "United States" + }, "entity": null, "sources": [], "aliases": [], diff --git a/scripts/build_series_catalog.py b/scripts/build_series_catalog.py index 39ad10a..5af1220 100644 --- a/scripts/build_series_catalog.py +++ b/scripts/build_series_catalog.py @@ -2,37 +2,46 @@ """Build ledger/series_catalog.json — the canonical series registry. The observation file records facts; this catalog records the SERIES those -facts belong to, one row per family, keyed by a UUID that is minted exactly -once and preserved across regenerations. Consumers (Thesis's docket, bill -mappers, permalink surfaces) refer to series by catalog UUID or canonical -concept and never mint parallel identities. - -Family derivation is deterministic: strip period tokens (and nothing else) -from ``source_record_id`` and ``measure.concept``, replacing each with a -``{P}`` placeholder. Release-vintage segments such as ``first_print`` or -``third_estimate`` are preserved — collapsing across vintages is a curation -judgment, done by hand-merging catalog rows (the surviving row keeps its -UUID; absorbed spellings move into ``aliases``). The same applies to -concept-spelling drift (e.g. ``abs.cpi.all_groups.yoy`` vs -``abs.cpi_indicator.allgroups.yoy``): the generator never merges distinct -spellings mechanically. - -Recognized period tokens (dotted segments): - fy2026 | 2026-05 | may_2026 | q1_2026 | 2026_q1 | week_2026-05-02 - -Usage: - python3 scripts/build_series_catalog.py # regenerate - python3 scripts/build_series_catalog.py --docket PATH # seed docket-only rows - python3 scripts/build_series_catalog.py --check # verify committed file is current - -Families whose post-strip concept is identical (patterns differing only in -``{P}`` placement, e.g. ``bls.cps.unemployment_rate`` observed both bare and -period-suffixed) are the same series and merge into one row listing every -observed pattern. - -Idempotent: same observations + same existing catalog -> byte-identical -output. New series mint fresh UUIDv4s; existing series keep theirs -(looked up by ``concept``, the stable identity key). +facts belong to, one row per (concept, geography, entity) identity — the +same identity dimensions the fact ADR uses — keyed by a UUID that is minted +exactly once and preserved across regenerations. Consumers (Thesis's docket, +bill mappers, permalink surfaces) refer to series by catalog UUID or concept +and never mint parallel identities. + +Family derivation strips period tokens (and nothing else) from +``source_record_id`` and ``measure.concept``, replacing each with ``{P}``. +Two passes strip a segment: a shape pass (the recognized token grammar +below) and a semantic pass (tokens derived from the row's own ``period``, +which catches spellings the grammar has not met yet). Segments that still +contain a four-digit year after both passes are reported in +``suspect_segments`` for curation — flagged, never silently stripped. + +Recognized token grammar (dotted segments): + fy2026 | 2026-05 | 2026_05 | 2026-05-02 | 2026_05_02 | may_2026 | feb_2026 + | q1_2026 | 2026_q1 | week_2026-05-02 | week_2026_05_02 + | week_ending_2026_05_02 | february_to_april_2026 + | after_june_2026 | after_mpc_june_2026 (period-qualifier compounds) + +Release-vintage segments (``first_print``, ``third_estimate``, +``original_submission``) are preserved: collapsing across vintages is a +curation judgment, done by hand-merging catalog rows (the surviving row +keeps its UUID; absorbed spellings move into ``aliases``). The same applies +to concept-spelling drift (``abs.cpi.all_groups.yoy`` vs +``abs.cpi_indicator.allgroups.yoy``): never merged mechanically. Curated +aliases persist across regeneration — the existing catalog's aliases are +unioned with derived ones, and alias matches inherit the existing UUID, so +a hand merge or an upstream concept rename does not remint identity. + +Inputs are pinned: the observation JSONL (digest in ``observations_sha256``) +and the committed docket seed at ``ledger/seeds/thesis_docket_series.json`` +(digest in ``docket_seed_sha256``), which contributes docket-only rows for +Thesis docket series not yet observed. Bare ``--check`` uses both committed +inputs, so CI needs no external files. + +Idempotent: same inputs + same existing catalog -> byte-identical output. +New identities mint fresh UUIDv4s; existing identities keep theirs (looked +up by identity key, then by concept/alias). Unit or cadence conflicts +within one identity are a hard error, never a silent modal pick. """ from __future__ import annotations @@ -43,20 +52,25 @@ import pathlib import re import sys -import uuid +import uuid as uuid_module from collections import Counter ROOT = pathlib.Path(__file__).resolve().parents[1] OBSERVATIONS = ROOT / "ledger" / "official_observations.jsonl" CATALOG = ROOT / "ledger" / "series_catalog.json" +DOCKET_SEED = ROOT / "ledger" / "seeds" / "thesis_docket_series.json" -GENERATOR_VERSION = 1 +GENERATOR_VERSION = 2 -MONTHS = [ - "", +MONTHS_FULL = [ "january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december", ] +MONTHS_ABBREV = [ + "jan", "feb", "mar", "apr", "may", "jun", + "jul", "aug", "sep", "sept", "oct", "nov", "dec", +] +_MONTH_ALT = "|".join(sorted(set(MONTHS_FULL + MONTHS_ABBREV), key=len, reverse=True)) # Docket cadence words -> ledger period types. CADENCE_TO_PERIOD_TYPE = { @@ -67,199 +81,299 @@ "fiscal_year": "fiscal_year", } +# ISO country codes as used by observed non-US rows; US uses the Census id. +COUNTRY_GEOGRAPHY = { + "US": {"level": "country", "id": "0100000US", "name": "United States"}, + "CA": {"level": "country", "id": "CA", "name": None}, + "GB": {"level": "country", "id": "GB", "name": None}, + "AU": {"level": "country", "id": "AU", "name": None}, + "JP": {"level": "country", "id": "JP", "name": None}, + "BE": {"level": "country", "id": "BE", "name": None}, +} + _PERIOD_SEGMENT = re.compile( - r"^(?:" - r"fy\d{4}" # fy2026 - r"|\d{4}-\d{2}(?:-\d{2})?" # 2026-05, 2026-05-02 - r"|(?:%s)_\d{4}" # may_2026 - r"|q[1-4]_\d{4}" # q1_2026 - r"|\d{4}_q[1-4]" # 2026_q1 - r"|week_\d{4}-\d{2}-\d{2}" # week_2026-05-02 - r")$" % "|".join(m for m in MONTHS if m) + r"^(?:after_(?:[a-z]+_)?)?" # after_june_2026, after_mpc_june_2026 + r"(?:" + r"fy\d{4}" # fy2026 + r"|\d{4}[-_]\d{2}(?:[-_]\d{2})?" # 2026-05, 2026_05, 2026-05-02, 2026_05_02 + r"|(?:%(m)s)_\d{4}" # may_2026, feb_2026 + r"|(?:%(m)s)_to_(?:%(m)s)_\d{4}" # february_to_april_2026 + r"|q[1-4]_\d{4}" # q1_2026 + r"|\d{4}_q[1-4]" # 2026_q1 + r"|week_(?:ending_)?\d{4}[-_]\d{2}[-_]\d{2}" # week_2026-05-02, week_ending_2026_06_06 + r")$" % {"m": _MONTH_ALT} ) +_YEAR_HINT = re.compile(r"(?:19|20)\d{2}") + def is_period_segment(segment: str) -> bool: - """Whether one dotted segment is a period token.""" + """Whether one dotted segment matches the period-token grammar. + + >>> [is_period_segment(s) for s in ( + ... "2026_05", "2026-05", "feb_2026", "week_ending_2026_06_06", + ... "after_june_2026", "after_mpc_june_2026", "2026_06_18", + ... "february_to_april_2026", "week_2026-06-13", "fy2026", "2026_q2", + ... )] + [True, True, True, True, True, True, True, True, True, True, True] + >>> [is_period_segment(s) for s in ( + ... "36-10-0434-01", "g17", "adv44x72", "j5ii", "m3", + ... "first_print", "third_estimate", "original_submission", + ... "total_nonfarm_payroll_change", "australia", + ... )] + [False, False, False, False, False, False, False, False, False, False] + """ return bool(_PERIOD_SEGMENT.fullmatch(segment)) -def family_pattern(identifier: str) -> str: - """Replace every period-token segment with ``{P}``. +def period_token_variants(period: dict) -> set[str]: + """Every spelling of ``period`` that may appear as an id segment.""" + ptype, value = period.get("type"), period.get("value") + tokens: set[str] = set() + if value is None: + return tokens + value = str(value) + if ptype == "fiscal_year": + tokens.add(f"fy{value}") + elif ptype == "month": + m = re.fullmatch(r"(\d{4})-(\d{2})", value) + if m: + year, month = m.group(1), int(m.group(2)) + tokens.update({f"{year}-{month:02d}", f"{year}_{month:02d}"}) + tokens.add(f"{MONTHS_FULL[month - 1]}_{year}") + tokens.add(f"{MONTHS_ABBREV[month - 1]}_{year}") + elif ptype == "quarter": + m = re.fullmatch(r"(\d{4})-(\d{2})", value) + if m: + year, quarter = m.group(1), (int(m.group(2)) - 1) // 3 + 1 + tokens.update({f"q{quarter}_{year}", f"{year}_q{quarter}"}) + elif ptype == "week_ending": + iso = value + tokens.update({ + f"week_{iso}", f"week_{iso.replace('-', '_')}", + f"week_ending_{iso}", f"week_ending_{iso.replace('-', '_')}", + }) + return tokens + + +def family_pattern(identifier: str, period: dict | None = None) -> str: + """Replace period-token segments with ``{P}``. >>> family_pattern("bls.eci.private_wages_salaries_qoq.2026_q2.first_print") 'bls.eci.private_wages_salaries_qoq.{P}.first_print' - >>> family_pattern("abs.labour.employment_change.australia.june_2026") - 'abs.labour.employment_change.australia.{P}' + >>> family_pattern("census.m3.durable_goods_new_orders_mom.2026_06") + 'census.m3.durable_goods_new_orders_mom.{P}' + >>> family_pattern("boe.bank_rate.after_mpc_june_2026") + 'boe.bank_rate.{P}' + >>> family_pattern("ons.labour.unemployment_rate.february_to_april_2026") + 'ons.labour.unemployment_rate.{P}' >>> family_pattern("abs.cpi.all_groups.yoy") 'abs.cpi.all_groups.yoy' - >>> family_pattern("usda.fsa.snap.participation.fy2026.october_2025") - 'usda.fsa.snap.participation.{P}.{P}' """ + derived = period_token_variants(period or {}) segments = identifier.split(".") - return ".".join("{P}" if is_period_segment(s) else s for s in segments) + return ".".join( + "{P}" if (is_period_segment(s) or s in derived) else s for s in segments + ) def concept_for(pattern: str) -> str: - """The human-facing canonical concept: the pattern minus its placeholders. - - >>> concept_for("bls.eci.private_wages_salaries_qoq.{P}.first_print") - 'bls.eci.private_wages_salaries_qoq.first_print' - >>> concept_for("abs.cpi.all_groups.yoy") - 'abs.cpi.all_groups.yoy' - """ + """The human-facing canonical concept: the pattern minus placeholders.""" return ".".join(s for s in pattern.split(".") if s != "{P}") -def _modal(counter: Counter) -> tuple[object, list]: - """Most common value plus the sorted list of variants (if more than one).""" - if not counter: - return None, [] - ranked = counter.most_common() - modal = ranked[0][0] - variants = sorted(str(v) for v, _ in ranked) - return modal, variants if len(ranked) > 1 else [] +def suspect_segments(pattern: str) -> list[str]: + """Post-strip segments that still smell of a date — flagged, not stripped.""" + return [ + s for s in pattern.split(".") + if s != "{P}" and _YEAR_HINT.search(s) + ] + + +def _geo_key(geography: dict | None) -> str: + g = geography or {} + return f"{g.get('level')}|{g.get('id')}" + + +def _entity_key(entity: dict | None) -> str: + e = entity or {} + return f"{e.get('name')}|{e.get('role')}" -def build_families(rows: list[dict]) -> dict[str, dict]: - """Group observation rows into families keyed by family_pattern.""" - families: dict[str, dict] = {} - for row in rows: +def build_identities(rows: list[dict]) -> dict[tuple[str, str, str], dict]: + """Group observation rows by (concept, geography, entity) identity.""" + identities: dict[tuple[str, str, str], dict] = {} + for index, row in enumerate(rows): rid = row.get("source_record_id") measure = row.get("measure") or {} concept_raw = measure.get("concept") if not isinstance(rid, str) or not isinstance(concept_raw, str): raise SystemExit( - "observation row missing source_record_id or measure.concept: " - f"{json.dumps(row)[:200]}" + f"observation row {index} missing source_record_id or " + f"measure.concept: {json.dumps(row)[:200]}" ) - pattern = family_pattern(concept_raw) - fam = families.setdefault( - pattern, + period = row.get("period") or {} + pattern = family_pattern(concept_raw, period) + geography = row.get("geography") or None + entity = row.get("entity") or None + key = (concept_for(pattern), _geo_key(geography), _entity_key(entity)) + ident = identities.setdefault( + key, { + "patterns": set(), "concepts": set(), "source_concepts": set(), "rid_patterns": set(), + "suspects": set(), "units": Counter(), "period_types": Counter(), - "geographies": Counter(), - "entities": Counter(), + "geography": geography, + "entity": entity, "sources": set(), "period_values": [], "count": 0, }, ) - fam["concepts"].add(concept_raw) + ident["patterns"].add(pattern) + ident["concepts"].add(concept_raw) source_concept = measure.get("source_concept") if isinstance(source_concept, str): - fam["source_concepts"].add(source_concept) - fam["rid_patterns"].add(family_pattern(rid)) - fam["units"][measure.get("unit")] += 1 - period = row.get("period") or {} - fam["period_types"][period.get("type")] += 1 - geography = row.get("geography") or {} - fam["geographies"][ - json.dumps( - {k: geography.get(k) for k in ("level", "id", "name")}, - sort_keys=True, - ) - ] += 1 - entity = row.get("entity") or {} - fam["entities"][json.dumps(entity, sort_keys=True)] += 1 + ident["source_concepts"].add(source_concept) + rid_pattern = family_pattern(rid, period) + ident["rid_patterns"].add(rid_pattern) + ident["suspects"].update(suspect_segments(pattern)) + ident["suspects"].update(suspect_segments(rid_pattern)) + ident["units"][measure.get("unit")] += 1 + ident["period_types"][period.get("type")] += 1 source = row.get("source") or {} if isinstance(source.get("source_name"), str): - fam["sources"].add(source["source_name"]) + ident["sources"].add(source["source_name"]) if period.get("value") is not None: - fam["period_values"].append(str(period["value"])) - fam["count"] += 1 - return families - - -def load_existing_uuids(path: pathlib.Path) -> dict[str, str]: - """concept -> uuid from the committed catalog, if present.""" - if not path.exists(): - return {} - catalog = json.loads(path.read_text(encoding="utf-8")) - return {row["concept"]: row["uuid"] for row in catalog.get("series", [])} + ident["period_values"].append(str(period["value"])) + ident["count"] += 1 + return identities + + +def _sole(counter: Counter, what: str, key: tuple) -> object: + """The single value in ``counter`` — conflicts are a hard error.""" + values = [v for v in counter if v is not None] + if len(values) > 1: + raise SystemExit( + f"{what} conflict within identity {key}: {sorted(map(str, values))} " + "— resolve by curation (split or correct upstream); the catalog " + "never picks a modal winner" + ) + return values[0] if values else None + + +class ExistingCatalog: + """UUID and curated-alias memory from the committed catalog.""" + + def __init__(self, path: pathlib.Path) -> None: + self.by_identity: dict[tuple[str, str, str], dict] = {} + self.by_concept: dict[str, list[dict]] = {} + self.by_alias: dict[str, list[dict]] = {} + if not path.exists(): + return + catalog = json.loads(path.read_text(encoding="utf-8")) + for row in catalog.get("series", []): + key = ( + row["concept"], + _geo_key(row.get("geography")), + _entity_key(row.get("entity")), + ) + self.by_identity[key] = row + self.by_concept.setdefault(row["concept"], []).append(row) + for alias in row.get("aliases", []): + self.by_alias.setdefault(alias, []).append(row) + + def match(self, key: tuple[str, str, str], names: set[str]) -> dict | None: + """The existing row for this identity: exact key, else unique name hit.""" + row = self.by_identity.get(key) + if row is not None: + return row + hits: dict[str, dict] = {} + for name in names: + for row in self.by_concept.get(name, []) + self.by_alias.get(name, []): + hits[row["uuid"]] = row + if len(hits) == 1: + return next(iter(hits.values())) + if len(hits) > 1: + raise SystemExit( + f"identity {key} matches multiple existing UUIDs via " + f"concept/alias names {sorted(names)}: {sorted(hits)} — " + "curate the existing rows (merge or disambiguate aliases) " + "before regenerating" + ) + return None def build_catalog( observations_path: pathlib.Path, docket_path: pathlib.Path | None, - existing_uuids: dict[str, str], + existing: ExistingCatalog, ) -> dict: raw = observations_path.read_bytes() rows = [json.loads(line) for line in raw.decode().splitlines() if line.strip()] - families = build_families(rows) - - # Merge families whose post-strip concept is identical: patterns that - # differ only in {P} placement are one series observed under two period - # formattings, not two series. - by_concept_key: dict[str, dict] = {} - for pattern in sorted(families): - fam = families[pattern] - key = concept_for(pattern) - merged = by_concept_key.setdefault( - key, - { - "patterns": set(), - "concepts": set(), - "source_concepts": set(), - "rid_patterns": set(), - "units": Counter(), - "period_types": Counter(), - "geographies": Counter(), - "entities": Counter(), - "sources": set(), - "period_values": [], - "count": 0, - }, - ) - merged["patterns"].add(pattern) - for field in ("concepts", "source_concepts", "rid_patterns", "sources"): - merged[field] |= fam[field] - for field in ("units", "period_types", "geographies", "entities"): - merged[field] += fam[field] - merged["period_values"] += fam["period_values"] - merged["count"] += fam["count"] + identities = build_identities(rows) series: list[dict] = [] - for key in sorted(by_concept_key): - fam = by_concept_key[key] - unit, unit_variants = _modal(fam["units"]) - period_type, period_variants = _modal(fam["period_types"]) - geography_json, _ = _modal(fam["geographies"]) - entity_json, _ = _modal(fam["entities"]) - row = { - "uuid": existing_uuids.get(key) or str(uuid.uuid4()), - "concept": key, - "family_patterns": sorted(fam["patterns"]), + used_uuids: dict[str, tuple] = {} + + def mint(key: tuple, names: set[str]) -> tuple[str, list[str]]: + prior = existing.match(key, names) + row_uuid = prior["uuid"] if prior else str(uuid_module.uuid4()) + curated_aliases = list(prior.get("aliases", [])) if prior else [] + if row_uuid in used_uuids: + raise SystemExit( + f"UUID collision: {row_uuid} claimed by both " + f"{used_uuids[row_uuid]} and {key} — curate the existing " + "catalog before regenerating" + ) + used_uuids[row_uuid] = key + return row_uuid, curated_aliases + + all_suspects: set[str] = set() + for key in sorted(identities): + ident = identities[key] + concept, _, _ = key + names = ident["concepts"] | ident["source_concepts"] | {concept} + row_uuid, curated_aliases = mint(key, names) + aliases = sorted( + (ident["concepts"] | ident["source_concepts"] | set(curated_aliases)) + - {concept} + ) + all_suspects.update(ident["suspects"]) + series.append({ + "uuid": row_uuid, + "concept": concept, + "family_patterns": sorted(ident["patterns"]), "status": "observed", - "unit": unit, - "cadence": period_type, - "geography": json.loads(geography_json) if geography_json else None, - "entity": json.loads(entity_json) if entity_json else None, - "sources": sorted(fam["sources"]), - "aliases": sorted( - (fam["concepts"] | fam["source_concepts"]) - {key} - ), - "rid_patterns": sorted(fam["rid_patterns"]), - "first_observed_period": min(fam["period_values"], default=None), - "last_observed_period": max(fam["period_values"], default=None), - "observation_count": fam["count"], - } - if unit_variants: - row["unit_variants"] = unit_variants - if period_variants: - row["cadence_variants"] = period_variants - series.append(row) - - if docket_path is not None: - docket = json.loads(docket_path.read_text(encoding="utf-8")) - by_concept = {row["concept"]: row for row in series} - alias_index = { - alias: row for row in series for alias in row["aliases"] - } + "unit": _sole(ident["units"], "unit", key), + "cadence": _sole(ident["period_types"], "cadence", key), + "geography": ident["geography"], + "entity": ident["entity"], + "sources": sorted(ident["sources"]), + "aliases": aliases, + "rid_patterns": sorted(ident["rid_patterns"]), + "first_observed_period": min(ident["period_values"], default=None), + "last_observed_period": max(ident["period_values"], default=None), + "observation_count": ident["count"], + }) + + docket_raw = b"" + if docket_path is not None and docket_path.exists(): + docket_raw = docket_path.read_bytes() + docket = json.loads(docket_raw.decode()) + alias_tally = Counter( + alias for row in series for alias in row["aliases"] + ) + claimed: dict[str, dict] = {} + for row in series: + claimed.setdefault(row["concept"], row) + for alias in row["aliases"]: + if alias_tally[alias] == 1: + claimed.setdefault(alias, row) for entry in docket["series"]: concept = entry["series"] cadence_word = entry.get("cadence") @@ -268,50 +382,74 @@ def build_catalog( f"docket cadence {cadence_word!r} for {concept} has no " "period-type mapping; extend CADENCE_TO_PERIOD_TYPE" ) - target_unit = (entry.get("extras") or {}).get("targetUnit") - hit = by_concept.get(concept) or alias_index.get(concept) + extras = entry.get("extras") or {} + hit = claimed.get(concept) if hit is not None: if concept != hit["concept"] and concept not in hit["aliases"]: hit["aliases"] = sorted(hit["aliases"] + [concept]) - if hit["unit"] is None and target_unit is not None: - hit["unit"] = target_unit continue + country = extras.get("country") + geography = None + if country is not None: + geography = COUNTRY_GEOGRAPHY.get(country) + if geography is None: + raise SystemExit( + f"docket entry {concept} has country {country!r} with " + "no geography mapping; extend COUNTRY_GEOGRAPHY" + ) + key = (concept, _geo_key(geography), _entity_key(None)) + row_uuid, curated_aliases = mint(key, {concept}) row = { - "uuid": existing_uuids.get(concept) or str(uuid.uuid4()), + "uuid": row_uuid, "concept": concept, "family_patterns": [family_pattern(concept)], "status": "docket-only", - "unit": target_unit, + "unit": extras.get("targetUnit"), "cadence": CADENCE_TO_PERIOD_TYPE[cadence_word], - "geography": None, + "geography": geography, "entity": None, "sources": [], - "aliases": [], + "aliases": sorted(curated_aliases), "rid_patterns": [], "first_observed_period": None, "last_observed_period": None, "observation_count": 0, } series.append(row) - by_concept[concept] = row + claimed[concept] = row + + series.sort(key=lambda row: ( + row["concept"], + _geo_key(row.get("geography")), + _entity_key(row.get("entity")), + )) + + alias_counts = Counter( + alias for row in series for alias in row["aliases"] + ) + ambiguous_aliases = sorted(a for a, n in alias_counts.items() if n > 1) - series.sort(key=lambda row: row["concept"]) - concepts = [row["concept"] for row in series] - if len(concepts) != len(set(concepts)): - dupes = sorted({c for c in concepts if concepts.count(c) > 1}) - raise SystemExit(f"duplicate concepts in catalog output: {dupes}") return { "comment": ( - "Canonical series catalog. One row per series family; uuid is " - "minted once and never re-minted (regeneration preserves it by " - "concept). Consumers reference series by uuid or concept " - "only. Regenerate with scripts/build_series_catalog.py; verify " - "with --check. Cross-spelling merges are manual curation: keep " - "the surviving row's uuid, move absorbed spellings to aliases." + "Canonical series catalog. One row per (concept, geography, " + "entity) identity; uuid is minted once and never re-minted " + "(regeneration preserves it by identity, then by concept/alias). " + "Consumers reference series by uuid or concept only. Regenerate " + "with scripts/build_series_catalog.py; verify with --check. " + "Cross-spelling and cross-vintage merges are manual curation: " + "keep the surviving row's uuid, move absorbed spellings to " + "aliases — curated aliases persist across regeneration. Aliases " + "listed in ambiguous_aliases match multiple rows and never " + "drive identity inheritance." ), "generator_version": GENERATOR_VERSION, "observations_sha256": hashlib.sha256(raw).hexdigest(), "observation_rows": len(rows), + "docket_seed_sha256": ( + hashlib.sha256(docket_raw).hexdigest() if docket_raw else None + ), + "suspect_segments": sorted(all_suspects), + "ambiguous_aliases": ambiguous_aliases, "series": series, } @@ -320,6 +458,26 @@ def render(catalog: dict) -> str: return json.dumps(catalog, indent=2, ensure_ascii=False) + "\n" +def validate_uuids(catalog: dict) -> list[str]: + """UUID syntax, version, and global-uniqueness problems.""" + problems: list[str] = [] + seen: dict[str, str] = {} + for row in catalog.get("series", []): + value = row.get("uuid", "") + concept = row.get("concept", "?") + try: + parsed = uuid_module.UUID(value) + except (ValueError, AttributeError, TypeError): + problems.append(f"{concept}: uuid {value!r} does not parse") + continue + if parsed.version != 4: + problems.append(f"{concept}: uuid {value} is not UUIDv4") + if value in seen: + problems.append(f"uuid {value} duplicated: {seen[value]} and {concept}") + seen[value] = concept + return problems + + def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--observations", type=pathlib.Path, default=OBSERVATIONS) @@ -327,8 +485,11 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument( "--docket", type=pathlib.Path, - default=None, - help="optional Thesis docket_series.json to seed docket-only rows", + default=DOCKET_SEED, + help=( + "Thesis docket seed for docket-only rows " + "(default: the committed seed; pass a path to update it)" + ), ) parser.add_argument( "--check", @@ -337,18 +498,33 @@ def main(argv: list[str] | None = None) -> int: ) args = parser.parse_args(argv) - existing = load_existing_uuids(args.catalog) + existing = ExistingCatalog(args.catalog) catalog = build_catalog(args.observations, args.docket, existing) body = render(catalog) + problems = validate_uuids(catalog) + if problems: + for problem in problems: + sys.stderr.write(f"uuid validation: {problem}\n") + return 1 + if args.check: - current = args.catalog.read_text(encoding="utf-8") if args.catalog.exists() else "" + current = ( + args.catalog.read_text(encoding="utf-8") + if args.catalog.exists() + else "" + ) if current != body: sys.stderr.write( "series_catalog.json is stale for these inputs; regenerate " "with scripts/build_series_catalog.py\n" ) return 1 + committed_problems = validate_uuids(json.loads(current)) + if committed_problems: + for problem in committed_problems: + sys.stderr.write(f"uuid validation: {problem}\n") + return 1 print(f"catalog current: {len(catalog['series'])} series") return 0 @@ -357,7 +533,9 @@ def main(argv: list[str] | None = None) -> int: docket_only = sum(1 for r in catalog["series"] if r["status"] == "docket-only") print( f"wrote {args.catalog}: {len(catalog['series'])} series " - f"({observed} observed, {docket_only} docket-only)" + f"({observed} observed, {docket_only} docket-only); " + f"suspects={len(catalog['suspect_segments'])}, " + f"ambiguous_aliases={len(catalog['ambiguous_aliases'])}" ) return 0 diff --git a/tests/test_build_series_catalog.py b/tests/test_build_series_catalog.py new file mode 100644 index 0000000..889a026 --- /dev/null +++ b/tests/test_build_series_catalog.py @@ -0,0 +1,220 @@ +"""Regression tests for the series-catalog generator. + +The token-grammar cases come from the 2026-08-01 adversarial review of the +first generator: every live period spelling it found unrecognized, plus the +non-period identifiers it confirmed must never be stripped. +""" + +from __future__ import annotations + +import json +import pathlib +import sys + +import pytest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "scripts")) + +import build_series_catalog as bsc # noqa: E402 + +PERIOD_SEGMENTS = [ + "fy2026", + "2026-05", + "2026_05", + "2026-05-02", + "2026_06_18", + "may_2026", + "feb_2026", + "q1_2026", + "2026_q2", + "week_2026-06-13", + "week_2026_06_13", + "week_ending_2026_06_06", + "february_to_april_2026", + "after_june_2026", + "after_mpc_june_2026", +] + +NON_PERIOD_SEGMENTS = [ + "36-10-0434-01", # StatCan table id + "g17", + "adv44x72", + "j5ii", + "m3", + "first_print", + "third_estimate", + "original_submission", + "australia", + "total_nonfarm_payroll_change", +] + + +@pytest.mark.parametrize("segment", PERIOD_SEGMENTS) +def test_period_segments_recognized(segment: str) -> None: + assert bsc.is_period_segment(segment), segment + + +@pytest.mark.parametrize("segment", NON_PERIOD_SEGMENTS) +def test_non_period_segments_retained(segment: str) -> None: + assert not bsc.is_period_segment(segment), segment + + +def test_semantic_pass_strips_unseen_spelling() -> None: + # A spelling the grammar does not know, derivable from the row period. + period = {"type": "month", "value": "2026-06"} + assert ( + bsc.family_pattern("agency.rate.june_2026", period) + == "agency.rate.{P}" + ) + + +def test_suspect_segments_flag_but_never_strip() -> None: + pattern = bsc.family_pattern("agency.series.mid2026wave") + assert pattern == "agency.series.mid2026wave" + assert bsc.suspect_segments(pattern) == ["mid2026wave"] + + +def _row( + concept: str, + *, + rid: str | None = None, + unit: str = "percent", + period: dict | None = None, + geography: dict | None = None, + entity: dict | None = None, + source_concept: str | None = None, +) -> dict: + period = period or {"type": "month", "value": "2026-05"} + return { + "value": 1.0, + "observed_at": "2026-06-01", + "period": period, + "geography": geography or {"level": "country", "id": "0100000US"}, + "entity": entity or {"name": "economy", "role": "aggregate"}, + "measure": { + "concept": concept, + "unit": unit, + **({"source_concept": source_concept} if source_concept else {}), + }, + "source": {"source_name": "test"}, + "source_record_id": rid or f"{concept}.first_print", + } + + +def _build(tmp_path: pathlib.Path, rows: list[dict], existing: dict | None = None): + observations = tmp_path / "obs.jsonl" + observations.write_text( + "".join(json.dumps(r) + "\n" for r in rows), encoding="utf-8" + ) + catalog_path = tmp_path / "catalog.json" + if existing is not None: + catalog_path.write_text(json.dumps(existing) + "\n", encoding="utf-8") + catalog = bsc.build_catalog( + observations, None, bsc.ExistingCatalog(catalog_path) + ) + return catalog + + +def test_geography_splits_identity(tmp_path: pathlib.Path) -> None: + rows = [ + _row("fns.snap.error_rate"), + _row( + "fns.snap.error_rate", + geography={"level": "state", "id": "0400000US06"}, + ), + ] + catalog = _build(tmp_path, rows) + assert len(catalog["series"]) == 2 + ids = {(r["geography"] or {}).get("id") for r in catalog["series"]} + assert ids == {"0100000US", "0400000US06"} + + +def test_rename_inherits_uuid_via_alias(tmp_path: pathlib.Path) -> None: + first = _build(tmp_path, [_row("bls.cps.unemployment_rate")]) + (tmp_path / "catalog.json").write_text(bsc.render(first), encoding="utf-8") + old_uuid = first["series"][0]["uuid"] + renamed = _build( + tmp_path, + [_row("bls.cps.jobless_rate", source_concept="bls.cps.unemployment_rate")], + existing=first, + ) + assert renamed["series"][0]["uuid"] == old_uuid + + +def test_curated_alias_persists_and_inherits(tmp_path: pathlib.Path) -> None: + first = _build(tmp_path, [_row("abs.cpi.all_groups.yoy")]) + row = first["series"][0] + row["aliases"] = sorted(row["aliases"] + ["abs.cpi_indicator.allgroups.yoy"]) + merged = _build( + tmp_path, + [_row("abs.cpi_indicator.allgroups.yoy")], + existing=first, + ) + assert merged["series"][0]["uuid"] == row["uuid"] + assert "abs.cpi_indicator.allgroups.yoy" in merged["series"][0]["aliases"] + + +def test_ambiguous_alias_match_is_a_hard_error(tmp_path: pathlib.Path) -> None: + existing = { + "series": [ + { + "uuid": "11111111-1111-4111-8111-111111111111", + "concept": "a.one", + "geography": None, + "entity": None, + "aliases": ["SHARED"], + }, + { + "uuid": "22222222-2222-4222-8222-222222222222", + "concept": "a.two", + "geography": None, + "entity": None, + "aliases": ["SHARED"], + }, + ] + } + with pytest.raises(SystemExit, match="multiple existing UUIDs"): + _build( + tmp_path, + [_row("a.three", source_concept="SHARED")], + existing=existing, + ) + + +def test_unit_conflict_is_a_hard_error(tmp_path: pathlib.Path) -> None: + rows = [ + _row("fed.rate", unit="percent"), + _row( + "fed.rate", + unit="index_points", + period={"type": "month", "value": "2026-06"}, + ), + ] + with pytest.raises(SystemExit, match="unit conflict"): + _build(tmp_path, rows) + + +def test_uuid_validation_catches_bad_and_duplicate() -> None: + catalog = { + "series": [ + {"uuid": "not-a-uuid", "concept": "a"}, + {"uuid": "33333333-3333-4333-8333-333333333333", "concept": "b"}, + {"uuid": "33333333-3333-4333-8333-333333333333", "concept": "c"}, + ] + } + problems = bsc.validate_uuids(catalog) + assert any("does not parse" in p for p in problems) + assert any("duplicated" in p for p in problems) + + +def test_committed_catalog_is_current_and_valid() -> None: + """The committed artifact must regenerate byte-identically (seeded) and + carry unique, parseable UUIDv4s.""" + committed = bsc.CATALOG.read_text(encoding="utf-8") + catalog = bsc.build_catalog( + bsc.OBSERVATIONS, bsc.DOCKET_SEED, bsc.ExistingCatalog(bsc.CATALOG) + ) + assert bsc.render(catalog) == committed + assert bsc.validate_uuids(json.loads(committed)) == [] + assert json.loads(committed)["suspect_segments"] == [] From 2859ecb9d6ba36a1f46df37c3713cfb75f3e6375 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 1 Aug 2026 08:00:58 -0400 Subject: [PATCH 03/11] Canonical concept follows the prior row on alias inheritance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An observed spelling that inherits identity through a curated alias or a unique prior concept/alias match now keeps the prior row's canonical concept — curation owns naming; observed spellings fold into aliases. Buckets landing on one canonical identity merge; inheriting two different prior UUIDs is a hard error. Co-Authored-By: Claude Fable 5 --- ledger/series_catalog.json | 402 ++++++++++++++++---------------- scripts/build_series_catalog.py | 100 ++++++-- 2 files changed, 278 insertions(+), 224 deletions(-) diff --git a/ledger/series_catalog.json b/ledger/series_catalog.json index 557fad4..e7799c9 100644 --- a/ledger/series_catalog.json +++ b/ledger/series_catalog.json @@ -15,7 +15,7 @@ ], "series": [ { - "uuid": "e344ff46-5b07-401e-9203-72fe4d643aef", + "uuid": "efbb2901-f8f8-4f5b-9835-56e5cc3e62ff", "concept": "abs.building_approvals.total_dwellings_mom.australia", "family_patterns": [ "abs.building_approvals.total_dwellings_mom.australia.{P}" @@ -48,7 +48,7 @@ "observation_count": 1 }, { - "uuid": "17f78cc1-f82a-44a6-9620-fc10c310f8f1", + "uuid": "92220fc5-fd61-46f5-b02d-467948889e2d", "concept": "abs.cpi.all_groups.yoy", "family_patterns": [ "abs.cpi.all_groups.yoy" @@ -80,7 +80,7 @@ "observation_count": 1 }, { - "uuid": "b4935b21-ee8b-4076-b498-7babcf57ba07", + "uuid": "88055a40-bfac-468f-bec6-fcc768e7fc7b", "concept": "abs.cpi.all_groups_annual_rate.australia", "family_patterns": [ "abs.cpi.all_groups_annual_rate.australia.{P}" @@ -113,7 +113,7 @@ "observation_count": 1 }, { - "uuid": "977a5286-88de-4ebe-82aa-2d3bca2d8360", + "uuid": "b4d4b454-c8a7-42a4-9a28-ce393b664717", "concept": "abs.cpi_indicator.allgroups.yoy", "family_patterns": [ "abs.cpi_indicator.allgroups.yoy.{P}" @@ -146,7 +146,7 @@ "observation_count": 1 }, { - "uuid": "b17f788a-411a-4d9c-bb45-68d412eddeed", + "uuid": "ab7e3632-e9d3-453e-bdc5-aa5a8dca77c9", "concept": "abs.labour.employment_change.australia", "family_patterns": [ "abs.labour.employment_change.australia.{P}" @@ -180,7 +180,7 @@ "observation_count": 2 }, { - "uuid": "f9067f72-71de-469c-8d18-c838e8ff0859", + "uuid": "443cb1bf-49d3-4e1e-9897-9556c8a8c768", "concept": "abs.labour.unemployment_rate", "family_patterns": [ "abs.labour.unemployment_rate" @@ -198,7 +198,7 @@ "observation_count": 0 }, { - "uuid": "15214884-48b0-49f1-9d17-4f9b60e96fce", + "uuid": "980c42b6-db11-4b46-a2b5-e7cd6c67527a", "concept": "abs.labour.unemployment_rate.australia", "family_patterns": [ "abs.labour.unemployment_rate.australia.{P}" @@ -232,7 +232,7 @@ "observation_count": 2 }, { - "uuid": "8f65877d-6acb-4ee6-9158-b5448c526f21", + "uuid": "1d3d8ca7-091e-4098-9abc-38bd906e7c7a", "concept": "bank_of_canada.overnight_rate", "family_patterns": [ "bank_of_canada.overnight_rate.{P}" @@ -264,7 +264,7 @@ "observation_count": 1 }, { - "uuid": "2b523a47-f470-4dc3-8d15-4797a709079d", + "uuid": "efc3259b-eb44-49c3-9bc6-e92fd7cf6243", "concept": "bea.core_pce.mom", "family_patterns": [ "bea.core_pce.mom" @@ -282,7 +282,7 @@ "observation_count": 0 }, { - "uuid": "ae903852-8f94-4f58-ab5e-27068c9d46e1", + "uuid": "ff8df856-08d4-4d5b-8228-e51cff7e1a03", "concept": "bea.disposable_personal_income.level", "family_patterns": [ "bea.disposable_personal_income.level.{P}" @@ -315,7 +315,7 @@ "observation_count": 1 }, { - "uuid": "c4172a11-ef6e-42eb-89c0-00da23b825e1", + "uuid": "5271091d-451c-47dc-a3af-5b1581af13ba", "concept": "bea.government_social_benefits.level", "family_patterns": [ "bea.government_social_benefits.level.{P}" @@ -348,7 +348,7 @@ "observation_count": 1 }, { - "uuid": "540cce9c-6111-4547-a248-cc786deb8e1c", + "uuid": "ed4dcaa2-e7aa-4696-98c2-b82fa81ee5d0", "concept": "bea.government_social_benefits.medicaid", "family_patterns": [ "bea.government_social_benefits.medicaid.{P}" @@ -381,7 +381,7 @@ "observation_count": 1 }, { - "uuid": "b96f7aad-ffee-4849-a032-1343728c321d", + "uuid": "c3bd2028-88b3-48e4-b648-aca7be6c0434", "concept": "bea.government_social_benefits.medicare", "family_patterns": [ "bea.government_social_benefits.medicare.{P}" @@ -414,7 +414,7 @@ "observation_count": 1 }, { - "uuid": "73f44532-772a-428e-a255-08699bdb8f4e", + "uuid": "e63e1fb2-2db5-4068-80da-8caf1a3faa93", "concept": "bea.government_social_benefits.social_security", "family_patterns": [ "bea.government_social_benefits.social_security.{P}" @@ -447,7 +447,7 @@ "observation_count": 1 }, { - "uuid": "db938f25-1086-4faa-a9b6-dc378fd35ad8", + "uuid": "174240ec-f8ff-417a-bf1c-bdf088268a7c", "concept": "bea.pce.core_mom", "family_patterns": [ "bea.pce.core_mom.{P}" @@ -480,7 +480,7 @@ "observation_count": 1 }, { - "uuid": "622be0bf-ddaf-4051-a1ea-58dc4a34a67b", + "uuid": "0bcbb3b9-801b-435e-8aa8-646c0ac76445", "concept": "bea.pce_price_index.monthly_change", "family_patterns": [ "bea.pce_price_index.monthly_change.{P}" @@ -513,7 +513,7 @@ "observation_count": 1 }, { - "uuid": "33f11e9e-085e-48e0-abb1-3a85f85f510c", + "uuid": "c05ca17a-461c-4ae4-b685-5e6ba8ff7710", "concept": "bea.personal_current_taxes.level", "family_patterns": [ "bea.personal_current_taxes.level.{P}" @@ -546,7 +546,7 @@ "observation_count": 1 }, { - "uuid": "1467ba0f-93fd-4727-96e7-09954992831b", + "uuid": "83094ec4-c890-435b-8a6a-83273c3e9077", "concept": "bea.real_gdp.saar.third_estimate", "family_patterns": [ "bea.real_gdp.saar.{P}.third_estimate" @@ -579,7 +579,7 @@ "observation_count": 1 }, { - "uuid": "1eabcc3f-c99a-4217-ba36-dcaeef5aef60", + "uuid": "7be75093-832c-4734-a002-a0ff19c610ce", "concept": "bea.trade.goods_services_deficit", "family_patterns": [ "bea.trade.goods_services_deficit" @@ -597,7 +597,7 @@ "observation_count": 0 }, { - "uuid": "fa09cffe-676f-4c84-8064-e9a7960338ad", + "uuid": "27202e84-7743-46ed-a6e8-2c907aa79a6c", "concept": "bea.wages_and_salaries.level", "family_patterns": [ "bea.wages_and_salaries.level.{P}" @@ -630,7 +630,7 @@ "observation_count": 1 }, { - "uuid": "5a39ebcb-26da-46f3-9483-b8ba708a776e", + "uuid": "0edb7ad9-0541-4bd2-9597-bffc9e73cd92", "concept": "bls.ces.average_hourly_earnings_private_monthly_change", "family_patterns": [ "bls.ces.average_hourly_earnings_private_monthly_change" @@ -662,7 +662,7 @@ "observation_count": 1 }, { - "uuid": "11271834-da12-4926-a4ce-9f996fb6e7df", + "uuid": "d8ab9f3a-aa0c-4b7f-89df-a0c9107cfdbb", "concept": "bls.ces.nonfarm_payrolls.change", "family_patterns": [ "bls.ces.nonfarm_payrolls.change" @@ -680,7 +680,7 @@ "observation_count": 0 }, { - "uuid": "88bb402f-81ab-43e1-b0d4-a3707d2cf10f", + "uuid": "c0515530-c56b-417d-9ead-aff423c87207", "concept": "bls.ces.total_nonfarm_payroll_change", "family_patterns": [ "bls.ces.total_nonfarm_payroll_change.{P}" @@ -713,7 +713,7 @@ "observation_count": 1 }, { - "uuid": "8735586f-8f28-43b3-92bf-d5f4f0cf06c8", + "uuid": "0b4f36be-2bc7-430b-b960-5df4011347bb", "concept": "bls.ces.total_nonfarm_payroll_change", "family_patterns": [ "bls.ces.total_nonfarm_payroll_change" @@ -743,7 +743,7 @@ "observation_count": 1 }, { - "uuid": "1678deac-7af3-4012-92d8-ca70d4e4ad8b", + "uuid": "d0dc87fb-fd6d-4234-832a-869d5b2d8f3d", "concept": "bls.cpi.owners_equivalent_rent_mom", "family_patterns": [ "bls.cpi.owners_equivalent_rent_mom" @@ -761,7 +761,7 @@ "observation_count": 0 }, { - "uuid": "f21957d5-0017-4a38-9ec0-81dc8ee06b75", + "uuid": "615ac1b1-27e8-437d-bf69-86d760488dc0", "concept": "bls.cpi.rent_primary_residence_mom", "family_patterns": [ "bls.cpi.rent_primary_residence_mom" @@ -779,7 +779,7 @@ "observation_count": 0 }, { - "uuid": "35f5c356-4da0-488f-8d4e-4edcf1c76536", + "uuid": "3bab6696-88c0-44c8-988c-737d2d00c37c", "concept": "bls.cpi.services_less_energy_mom", "family_patterns": [ "bls.cpi.services_less_energy_mom" @@ -797,7 +797,7 @@ "observation_count": 0 }, { - "uuid": "13afafd9-60cc-41e7-a3eb-2e6e601bae64", + "uuid": "975e942b-d456-480d-859f-a9ffaffcfe34", "concept": "bls.cpi.services_less_rent_shelter_mom", "family_patterns": [ "bls.cpi.services_less_rent_shelter_mom" @@ -815,7 +815,7 @@ "observation_count": 0 }, { - "uuid": "e26bb549-b24e-4971-93d4-4794f70d87d1", + "uuid": "4c5b4292-0bcf-4c42-9fc5-39d22e791f75", "concept": "bls.cpi.shelter_mom", "family_patterns": [ "bls.cpi.shelter_mom" @@ -833,7 +833,7 @@ "observation_count": 0 }, { - "uuid": "64198097-ede9-477c-8f3c-d5b247b2dae4", + "uuid": "e5e402d5-1a65-4c5d-a67a-07e3cf03cbf8", "concept": "bls.cpi.u.core_mom", "family_patterns": [ "bls.cpi.u.core_mom.{P}" @@ -866,7 +866,7 @@ "observation_count": 1 }, { - "uuid": "f98732c4-59d1-4bb1-94dd-e23ea9db3775", + "uuid": "d85c3c7f-874b-4caa-bfad-1ec7a473c293", "concept": "bls.cpi.u.core_mom", "family_patterns": [ "bls.cpi.u.core_mom.{P}" @@ -898,7 +898,7 @@ "observation_count": 1 }, { - "uuid": "4b37d4ee-446a-449c-be74-59177dec5d25", + "uuid": "70ca2ecf-4323-4453-b42f-23350cb95f22", "concept": "bls.cpi.u.headline_mom", "family_patterns": [ "bls.cpi.u.headline_mom.{P}" @@ -931,7 +931,7 @@ "observation_count": 1 }, { - "uuid": "41b3f275-2b6a-4e5b-a70a-a5dae8c83be6", + "uuid": "3e796803-a194-4c83-9dc9-27cc870ff08e", "concept": "bls.cpi.u.headline_mom", "family_patterns": [ "bls.cpi.u.headline_mom.{P}" @@ -963,7 +963,7 @@ "observation_count": 1 }, { - "uuid": "13cb7ad3-eceb-4c76-964f-022171881849", + "uuid": "3cbdbc7f-3e6b-4b76-8758-57e519906bd4", "concept": "bls.cps.employed_people_by_occupation.business_financial_operations", "family_patterns": [ "bls.cps.employed_people_by_occupation.business_financial_operations.{P}" @@ -996,7 +996,7 @@ "observation_count": 1 }, { - "uuid": "0284504d-9da8-44e2-b223-dfc2bac838a8", + "uuid": "54f19551-6288-48e2-9fa0-17933e55d390", "concept": "bls.cps.employed_people_by_occupation.computer_mathematical", "family_patterns": [ "bls.cps.employed_people_by_occupation.computer_mathematical.{P}" @@ -1029,7 +1029,7 @@ "observation_count": 1 }, { - "uuid": "d1c55677-6436-407b-9632-4d23389f9650", + "uuid": "25457957-df59-4e16-ae3f-431b4c63e7bf", "concept": "bls.cps.employed_people_by_occupation.healthcare_support", "family_patterns": [ "bls.cps.employed_people_by_occupation.healthcare_support.{P}" @@ -1062,7 +1062,7 @@ "observation_count": 1 }, { - "uuid": "ec4f8edd-1a44-4f51-a74b-77aec571445e", + "uuid": "2316f164-f13f-4025-980c-b92cc46b4d1a", "concept": "bls.cps.employed_people_by_occupation.office_administrative_support", "family_patterns": [ "bls.cps.employed_people_by_occupation.office_administrative_support.{P}" @@ -1095,7 +1095,7 @@ "observation_count": 1 }, { - "uuid": "ad9fb3ff-8e30-418b-837c-83b572fb526b", + "uuid": "3240539a-c5b8-43de-a240-cc6754fb59aa", "concept": "bls.cps.employed_people_by_occupation.production", "family_patterns": [ "bls.cps.employed_people_by_occupation.production.{P}" @@ -1128,7 +1128,7 @@ "observation_count": 1 }, { - "uuid": "747b0c83-5c43-463d-8f1c-804726d7e92f", + "uuid": "e2502908-1e16-46e3-b330-4d6612e510c2", "concept": "bls.cps.employed_people_by_occupation.transportation_material_moving", "family_patterns": [ "bls.cps.employed_people_by_occupation.transportation_material_moving.{P}" @@ -1161,7 +1161,7 @@ "observation_count": 1 }, { - "uuid": "fa85d459-e276-4aa7-a31e-c29c078ea8ef", + "uuid": "94a5a046-ea77-449a-8ba9-95ffebc9039c", "concept": "bls.cps.telework_share", "family_patterns": [ "bls.cps.telework_share" @@ -1179,7 +1179,7 @@ "observation_count": 0 }, { - "uuid": "1804da11-bfb6-4788-96ee-b966f0193e49", + "uuid": "e0be267e-d74c-4ad2-a5cd-5ac9f821e018", "concept": "bls.cps.u6_underemployment_rate", "family_patterns": [ "bls.cps.u6_underemployment_rate" @@ -1197,7 +1197,7 @@ "observation_count": 0 }, { - "uuid": "d5277474-5aa3-43ec-8985-e5abe6cc4210", + "uuid": "db2f3857-e0fa-48f8-9ab1-6c8989307fd2", "concept": "bls.cps.unemployment_rate", "family_patterns": [ "bls.cps.unemployment_rate.{P}" @@ -1230,7 +1230,7 @@ "observation_count": 1 }, { - "uuid": "efa1b3c9-4826-4179-8b79-92cda290d009", + "uuid": "8dbbd54f-4bfd-4735-ad0d-5b55b8bb4ec5", "concept": "bls.cps.unemployment_rate", "family_patterns": [ "bls.cps.unemployment_rate" @@ -1260,7 +1260,7 @@ "observation_count": 1 }, { - "uuid": "cbc7c2f1-ba6f-4fd3-937e-1e1501a4d77e", + "uuid": "6d2e1191-9e14-4719-a2bb-c3561fca49e6", "concept": "bls.eci.private_wages_salaries_qoq", "family_patterns": [ "bls.eci.private_wages_salaries_qoq.{P}" @@ -1293,7 +1293,7 @@ "observation_count": 1 }, { - "uuid": "b1369ea9-86d6-4af7-b8f0-50717149c38f", + "uuid": "2820e965-4c7b-40d2-b5b8-e03792cf3fbf", "concept": "bls.eci.total_compensation_private_industry_qoq", "family_patterns": [ "bls.eci.total_compensation_private_industry_qoq.{P}" @@ -1326,7 +1326,7 @@ "observation_count": 1 }, { - "uuid": "2392247f-91ea-4ecc-8b65-11bc3f88e220", + "uuid": "7dc8b980-8f14-4581-ad7c-3963de6a42d9", "concept": "bls.export_prices.all_commodities_mom", "family_patterns": [ "bls.export_prices.all_commodities_mom" @@ -1344,7 +1344,7 @@ "observation_count": 0 }, { - "uuid": "5622048a-7755-4f46-824d-160794cd2542", + "uuid": "1d271205-0e71-4a74-9d5f-47e77577e2b2", "concept": "bls.import_price_index.all_imports_mom", "family_patterns": [ "bls.import_price_index.all_imports_mom.{P}" @@ -1377,7 +1377,7 @@ "observation_count": 1 }, { - "uuid": "b2b0a6cc-9a31-456a-a28d-9ef4fde19406", + "uuid": "e8e6e43e-2c46-4620-a070-91d2da004b1c", "concept": "bls.import_price_index.all_imports_mom", "family_patterns": [ "bls.import_price_index.all_imports_mom.{P}" @@ -1410,7 +1410,7 @@ "observation_count": 1 }, { - "uuid": "bef0c281-ae00-4af2-8f92-54b23938f887", + "uuid": "3bddc6d0-e581-42dc-9c13-b731a9ede21f", "concept": "bls.jolts.hires_rate", "family_patterns": [ "bls.jolts.hires_rate" @@ -1428,7 +1428,7 @@ "observation_count": 0 }, { - "uuid": "75f43d09-f7bd-42f9-98dd-79a20ab8e994", + "uuid": "020dafc2-2ca3-453b-a6bc-f96d443a5f77", "concept": "bls.jolts.job_openings", "family_patterns": [ "bls.jolts.job_openings.{P}" @@ -1461,7 +1461,7 @@ "observation_count": 1 }, { - "uuid": "8c40edce-469c-4852-9564-e138932ce996", + "uuid": "1d05d94d-0288-46f0-b6a9-1fa499e85fff", "concept": "bls.jolts.job_openings_total", "family_patterns": [ "bls.jolts.job_openings_total.{P}" @@ -1494,7 +1494,7 @@ "observation_count": 1 }, { - "uuid": "ebc8ff13-5db1-4bfc-a921-58a7bfe748fa", + "uuid": "e6aed5ae-2f23-413b-a600-4c5b80edeb18", "concept": "bls.jolts.quits_rate", "family_patterns": [ "bls.jolts.quits_rate" @@ -1512,7 +1512,7 @@ "observation_count": 0 }, { - "uuid": "41380405-1391-475e-86ce-edc5fbf1ecf1", + "uuid": "0006af01-72ba-4e63-8d88-54500588f215", "concept": "bls.lns11300000", "family_patterns": [ "bls.lns11300000" @@ -1530,7 +1530,7 @@ "observation_count": 0 }, { - "uuid": "ef49ff78-b4ed-440d-97fa-4631b59f9a15", + "uuid": "a7ec4618-1ade-4f5e-bbbd-1c0680c7fccd", "concept": "bls.ppi.final_demand_monthly_change", "family_patterns": [ "bls.ppi.final_demand_monthly_change.{P}" @@ -1562,7 +1562,7 @@ "observation_count": 1 }, { - "uuid": "db5162f5-39d0-47a4-829d-f65fffd2b79a", + "uuid": "6e671a89-65f6-4da9-b092-4e6d74a36a08", "concept": "bls.productivity.nonfarm_qoq_prelim", "family_patterns": [ "bls.productivity.nonfarm_qoq_prelim" @@ -1580,7 +1580,7 @@ "observation_count": 0 }, { - "uuid": "14944fa7-9ceb-45c4-856d-a47d24cdcf08", + "uuid": "205885c4-de46-47df-a916-253ae2a0299a", "concept": "bls.productivity.nonfarm_unit_labor_costs_qoq_prelim", "family_patterns": [ "bls.productivity.nonfarm_unit_labor_costs_qoq_prelim" @@ -1598,7 +1598,7 @@ "observation_count": 0 }, { - "uuid": "90bf8f8b-40b9-4b86-91b1-c64ecaf73bdb", + "uuid": "2e3a7126-41fd-4d88-99b6-cf4f2057c277", "concept": "bls.real_earnings.avg_hourly_mom", "family_patterns": [ "bls.real_earnings.avg_hourly_mom" @@ -1616,7 +1616,7 @@ "observation_count": 0 }, { - "uuid": "27b594ab-ad4d-470d-aa70-99f159f978e1", + "uuid": "8fb890ac-38d0-430d-9a35-bbeffa0cf8ca", "concept": "boe.bank_rate", "family_patterns": [ "boe.bank_rate.{P}" @@ -1650,7 +1650,7 @@ "observation_count": 2 }, { - "uuid": "a27769be-70a4-40ba-9c2b-afd2689468a5", + "uuid": "e300b533-d2f0-41b4-8cbb-59168f8c80bd", "concept": "boj.policy_rate_guideline", "family_patterns": [ "boj.policy_rate_guideline.{P}" @@ -1683,7 +1683,7 @@ "observation_count": 1 }, { - "uuid": "aa0f4904-a802-4a63-afac-d259f650abe5", + "uuid": "63222847-045a-4784-bd89-4d7878a2a6d8", "concept": "census.construction_spending.total_mom", "family_patterns": [ "census.construction_spending.total_mom" @@ -1701,7 +1701,7 @@ "observation_count": 0 }, { - "uuid": "8bc25a3b-fc09-4545-a543-fa2ec8e2f824", + "uuid": "e16080e3-02e9-4ae1-95c6-3ded737cf697", "concept": "census.housing.completions_saar", "family_patterns": [ "census.housing.completions_saar" @@ -1719,7 +1719,7 @@ "observation_count": 0 }, { - "uuid": "d2bf0b63-8c7b-415a-8bfc-f971ad464899", + "uuid": "420c8935-70b8-4d8f-a82d-e772446adc6e", "concept": "census.housing.permits_saar", "family_patterns": [ "census.housing.permits_saar" @@ -1737,7 +1737,7 @@ "observation_count": 0 }, { - "uuid": "f8fae70e-07e5-455c-8e2c-b0cf15cd99d1", + "uuid": "d0212a1e-b360-460a-80a8-1cf4ad5ce427", "concept": "census.housing_starts.saar", "family_patterns": [ "census.housing_starts.saar.{P}" @@ -1769,7 +1769,7 @@ "observation_count": 1 }, { - "uuid": "e25e92f0-27d7-4de0-a096-bf745df47f0b", + "uuid": "8c999902-84e2-4e90-b49c-325cd535062a", "concept": "census.housing_starts.saar", "family_patterns": [ "census.housing_starts.saar.{P}" @@ -1802,7 +1802,7 @@ "observation_count": 1 }, { - "uuid": "3af7cdbd-2a38-4dca-8ff4-edadfa79de20", + "uuid": "294d82e8-3711-4d0d-89f6-a5a7a087f1be", "concept": "census.m3.durable_goods_new_orders_mom", "family_patterns": [ "census.m3.durable_goods_new_orders_mom.{P}" @@ -1835,7 +1835,7 @@ "observation_count": 1 }, { - "uuid": "8112793f-5172-4cb5-83f1-afd909626624", + "uuid": "5cbc4c1a-e387-4b64-bb56-67ef349fbd51", "concept": "census.m3.durable_goods_shipments_mom", "family_patterns": [ "census.m3.durable_goods_shipments_mom.{P}" @@ -1868,7 +1868,7 @@ "observation_count": 1 }, { - "uuid": "7c15316d-632c-40da-9c61-25b92a7531aa", + "uuid": "ed3215ed-43ec-44dc-b11c-f72a098fc008", "concept": "census.marts.adv44x72.monthly_change", "family_patterns": [ "census.marts.adv44x72.{P}.monthly_change" @@ -1901,7 +1901,7 @@ "observation_count": 1 }, { - "uuid": "686e3197-53ec-4988-ba8f-23c2bb90a902", + "uuid": "26bb1475-8685-4142-8472-7bcd6d998fcc", "concept": "census.mtis.total_business_inventories_level", "family_patterns": [ "census.mtis.total_business_inventories_level.{P}" @@ -1934,7 +1934,7 @@ "observation_count": 1 }, { - "uuid": "956247d0-69dd-4bde-859f-28b13905d719", + "uuid": "5654403e-f6c5-412e-94e0-feeda778ad81", "concept": "census.new_residential_sales.new_single_family_houses_sold_saar", "family_patterns": [ "census.new_residential_sales.new_single_family_houses_sold_saar" @@ -1952,7 +1952,7 @@ "observation_count": 0 }, { - "uuid": "b69629ad-dd6a-4bfe-96b6-f92b99017d4d", + "uuid": "aafb4ff0-cb04-4f76-96de-ef0baaa7bd93", "concept": "cms.care_compare.nursing_home_occupancy_pct", "family_patterns": [ "cms.care_compare.nursing_home_occupancy_pct.{P}" @@ -1985,7 +1985,7 @@ "observation_count": 1 }, { - "uuid": "24d9d68f-2ebc-44cf-b825-cb42f12d1103", + "uuid": "fc7c5e2f-90ff-4a9b-95cb-8d339fc8a89d", "concept": "cms.medicaid_pi.beneficiaries_disenrolled_procedural", "family_patterns": [ "cms.medicaid_pi.beneficiaries_disenrolled_procedural" @@ -2017,7 +2017,7 @@ "observation_count": 1 }, { - "uuid": "10692e21-bd74-47a7-a730-817da7aaec02", + "uuid": "8829b01a-9f45-4c9e-b38e-d04f405f238c", "concept": "cms.medicaid_pi.beneficiaries_disenrolled_total", "family_patterns": [ "cms.medicaid_pi.beneficiaries_disenrolled_total" @@ -2049,7 +2049,7 @@ "observation_count": 1 }, { - "uuid": "24c9025c-770e-41b2-a701-6dd3fafa7822", + "uuid": "56baf17e-830f-40ea-82f3-aa05cbbeeea2", "concept": "cms.medicaid_pi.beneficiaries_renewed_ex_parte", "family_patterns": [ "cms.medicaid_pi.beneficiaries_renewed_ex_parte" @@ -2081,7 +2081,7 @@ "observation_count": 1 }, { - "uuid": "ffe51507-ce80-4750-825d-087aeb8797de", + "uuid": "f6053113-5c2f-4832-9f54-e8ba5763f160", "concept": "cms.medicaid_pi.beneficiaries_renewed_total", "family_patterns": [ "cms.medicaid_pi.beneficiaries_renewed_total" @@ -2113,7 +2113,7 @@ "observation_count": 1 }, { - "uuid": "fbff6bd3-f4fc-42ae-b4d7-d39b9abf4f42", + "uuid": "35d1d31f-ade4-4487-a14c-bd492de01521", "concept": "cms.nursing_home_compare.reported_total_nurse_staffing_hprd_us", "family_patterns": [ "cms.nursing_home_compare.reported_total_nurse_staffing_hprd_us.{P}" @@ -2146,7 +2146,7 @@ "observation_count": 1 }, { - "uuid": "34291f59-cfc7-40ed-bf55-ab923b184b9a", + "uuid": "38773338-f84a-466e-94cb-fce55f5e2549", "concept": "dol.eta.continued_claims.sa", "family_patterns": [ "dol.eta.continued_claims.sa" @@ -2178,7 +2178,7 @@ "observation_count": 4 }, { - "uuid": "b77d7372-5127-4071-b927-e3f9452edc82", + "uuid": "330ec66b-502c-4ddf-9978-02199bab0055", "concept": "dol.eta.initial_claims.sa", "family_patterns": [ "dol.eta.initial_claims.sa.{P}" @@ -2210,7 +2210,7 @@ "observation_count": 1 }, { - "uuid": "92d57201-4c72-4f51-90d5-d77a910662d0", + "uuid": "32b335ea-b2dd-4fba-8321-e9751df19318", "concept": "ecb.deposit_facility_rate", "family_patterns": [ "ecb.deposit_facility_rate.{P}" @@ -2242,7 +2242,7 @@ "observation_count": 1 }, { - "uuid": "1b70b9e5-c422-4a91-ac1d-660012f09c12", + "uuid": "0be25641-eae9-45b8-b0c5-218b21e23d61", "concept": "estat.jp.cpi.core_exfreshfood.yoy", "family_patterns": [ "estat.jp.cpi.core_exfreshfood.yoy.{P}" @@ -2275,7 +2275,7 @@ "observation_count": 1 }, { - "uuid": "d69ffa04-6d46-4f4e-b9c9-da2c4e9bfbdd", + "uuid": "4eaed6f8-7da3-405d-8293-4b61d5cefc6f", "concept": "eurostat.ea.hicp.flash.yoy", "family_patterns": [ "eurostat.ea.hicp.flash.yoy", @@ -2309,7 +2309,7 @@ "observation_count": 2 }, { - "uuid": "e6dd2d67-f47a-4014-b591-a8fdb55f6cc7", + "uuid": "815299ce-841a-4115-9af2-3b52fc036834", "concept": "eurostat.hicp.all_items_annual_rate.euro_area", "family_patterns": [ "eurostat.hicp.all_items_annual_rate.euro_area.{P}" @@ -2341,7 +2341,7 @@ "observation_count": 1 }, { - "uuid": "0a6b3f6e-c324-4eb1-9561-218ed26cb2bc", + "uuid": "74ce2da4-28ed-409a-a0f1-2a7c40dc7caf", "concept": "eurostat.hicp.all_items_annual_rate.euro_area", "family_patterns": [ "eurostat.hicp.all_items_annual_rate.euro_area.{P}" @@ -2374,7 +2374,7 @@ "observation_count": 1 }, { - "uuid": "de81f2f8-dc4f-47ed-b94d-fea62024fc71", + "uuid": "1e84eb13-9e80-4dda-9b82-71361b886c1e", "concept": "eurostat.hicp.flash.yoy", "family_patterns": [ "eurostat.hicp.flash.yoy" @@ -2392,7 +2392,7 @@ "observation_count": 0 }, { - "uuid": "51bda8a2-7a5e-4643-87db-b66643485b8e", + "uuid": "4994dab0-bc46-4dd0-9bb7-ef716dfc4c75", "concept": "eurostat.industrial_production.euro_area", "family_patterns": [ "eurostat.industrial_production.euro_area.{P}" @@ -2424,7 +2424,7 @@ "observation_count": 1 }, { - "uuid": "704fd1b0-0fa1-415f-868b-cf2c2f57178c", + "uuid": "16b32f44-da0e-43de-bfb2-8b5a2a883b95", "concept": "eurostat.retail_trade.volume_mom.euro_area", "family_patterns": [ "eurostat.retail_trade.volume_mom.euro_area.{P}" @@ -2457,7 +2457,7 @@ "observation_count": 1 }, { - "uuid": "72f86cdc-c980-4dcd-95ef-7970eae2658f", + "uuid": "2c59f506-873f-45df-b6c5-588c82dcd640", "concept": "eurostat.unemployment_rate", "family_patterns": [ "eurostat.unemployment_rate" @@ -2475,7 +2475,7 @@ "observation_count": 0 }, { - "uuid": "14c8f455-9bb9-49aa-8ba2-83aaa7c42da8", + "uuid": "c7f994ce-3d0b-4a28-ba5b-e286237cd14d", "concept": "eurostat.unemployment_rate.belgium", "family_patterns": [ "eurostat.unemployment_rate.belgium" @@ -2497,7 +2497,7 @@ "observation_count": 0 }, { - "uuid": "be52503e-8272-4f00-b0d7-77b97501cf8f", + "uuid": "ec04c486-d8fd-4b1b-9841-884bed8d88c7", "concept": "eurostat.unemployment_rate.euro_area", "family_patterns": [ "eurostat.unemployment_rate.euro_area.{P}" @@ -2530,7 +2530,7 @@ "observation_count": 1 }, { - "uuid": "08765b77-5015-4965-a990-6928f36d4994", + "uuid": "40f48c12-3611-4e57-9482-055e62bf35ae", "concept": "fed.g17.capacity_utilization.manufacturing", "family_patterns": [ "fed.g17.capacity_utilization.manufacturing" @@ -2548,7 +2548,7 @@ "observation_count": 0 }, { - "uuid": "e535db92-cb5a-4767-8571-5d1c6fb507d3", + "uuid": "9668ecdb-5f61-422d-a653-8e8956257ade", "concept": "fed.g17.capacity_utilization.total_industry", "family_patterns": [ "fed.g17.capacity_utilization.total_industry.{P}" @@ -2581,7 +2581,7 @@ "observation_count": 1 }, { - "uuid": "f7a68fda-aebc-4b59-99b8-5bd30054a265", + "uuid": "978252a9-c452-41eb-aece-323340e796fc", "concept": "fed.g17.capacity_utilization.total_industry", "family_patterns": [ "fed.g17.capacity_utilization.total_industry.{P}" @@ -2613,7 +2613,7 @@ "observation_count": 1 }, { - "uuid": "84062e30-fb48-4ca2-8ade-d85a48f60116", + "uuid": "dbfd5050-e5df-40af-8922-dc8927d6363a", "concept": "fed.g17.industrial_production.total_index_mom", "family_patterns": [ "fed.g17.industrial_production.total_index_mom.{P}" @@ -2646,7 +2646,7 @@ "observation_count": 1 }, { - "uuid": "9cd54337-5f6e-4235-ba3b-1b84ec98c1df", + "uuid": "d5634fc4-dbe0-4203-a5b7-728708b85a14", "concept": "fed.g17.industrial_production.total_index_mom", "family_patterns": [ "fed.g17.industrial_production.total_index_mom.{P}" @@ -2678,7 +2678,7 @@ "observation_count": 1 }, { - "uuid": "9bea1d3b-1f9c-42d1-b260-dc01907d59a5", + "uuid": "41d26958-709c-44ab-a5ab-b385157db0ac", "concept": "fed.g17.manufacturing_production_mom", "family_patterns": [ "fed.g17.manufacturing_production_mom" @@ -2696,7 +2696,7 @@ "observation_count": 0 }, { - "uuid": "57d9986e-d045-4a1e-a92e-1ef6663935cf", + "uuid": "bd0643c4-6716-41ae-988d-c985272e38ae", "concept": "fed.g19.consumer_credit_nonrevolving_annual_rate", "family_patterns": [ "fed.g19.consumer_credit_nonrevolving_annual_rate" @@ -2714,7 +2714,7 @@ "observation_count": 0 }, { - "uuid": "7bc4723e-f169-44e2-a5b5-e7f3d898ff65", + "uuid": "8a82c6a5-7c83-4ac8-ab5b-7dd6eaf97e8f", "concept": "fed.g19.consumer_credit_revolving_annual_rate", "family_patterns": [ "fed.g19.consumer_credit_revolving_annual_rate" @@ -2732,7 +2732,7 @@ "observation_count": 0 }, { - "uuid": "ca1e40a0-25be-4e48-87c3-f7cad75d61cb", + "uuid": "dac76c03-0630-4859-898d-b4849383d147", "concept": "fed.g19.consumer_credit_total_annual_rate", "family_patterns": [ "fed.g19.consumer_credit_total_annual_rate" @@ -2750,7 +2750,7 @@ "observation_count": 0 }, { - "uuid": "0a1fc3c0-f785-4de8-a11d-aa41a4723dd4", + "uuid": "3b195edb-1b2d-4380-8267-43084fe3cbeb", "concept": "fns.snap.application_processing_timeliness_rate", "family_patterns": [ "fns.snap.application_processing_timeliness_rate" @@ -2780,7 +2780,7 @@ "observation_count": 1 }, { - "uuid": "842e02b6-e337-498a-92f0-b93846f1e3f8", + "uuid": "ae4cc9d0-e0f7-4de4-97bc-dfcf5afeb90e", "concept": "fns.snap.overpayment_error_rate", "family_patterns": [ "fns.snap.overpayment_error_rate" @@ -2810,7 +2810,7 @@ "observation_count": 1 }, { - "uuid": "494823b3-1b77-47fb-b32a-ddfabc80823d", + "uuid": "49b9c81b-935d-4523-a05d-d2000f8064e4", "concept": "fns.snap.share_jurisdictions_at_or_above_6pct", "family_patterns": [ "fns.snap.share_jurisdictions_at_or_above_6pct" @@ -2842,7 +2842,7 @@ "observation_count": 1 }, { - "uuid": "c585a927-4655-47a3-bde5-f84910a67b04", + "uuid": "d93e4cbb-5810-4a89-80fe-3830df22e057", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -2873,7 +2873,7 @@ "observation_count": 2 }, { - "uuid": "ddb40496-e5be-45d0-96fc-554d5f8a34a3", + "uuid": "0902c609-38ed-4b39-94bf-13aa8f5d6cbb", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -2903,7 +2903,7 @@ "observation_count": 1 }, { - "uuid": "02726f8d-c850-4246-b4fa-097f875f373f", + "uuid": "3b0cdfd9-16a4-424b-99ea-dbbcedecefd7", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -2933,7 +2933,7 @@ "observation_count": 1 }, { - "uuid": "260f07f5-f86e-4c9f-b1b1-3e8c1eee0541", + "uuid": "6c19d194-6a6b-4e7a-9fb1-f98ab24e6095", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -2963,7 +2963,7 @@ "observation_count": 1 }, { - "uuid": "0427fc19-e6a4-4aae-838a-92b97a98372f", + "uuid": "6409e147-6d79-4c75-ba50-7601bd7021dd", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -2993,7 +2993,7 @@ "observation_count": 1 }, { - "uuid": "0e7e38c9-86e8-46f0-94dd-96306f6d7493", + "uuid": "93d432d3-0c95-4924-a180-c74622391117", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3023,7 +3023,7 @@ "observation_count": 1 }, { - "uuid": "6f729ed5-8c47-47e5-8665-296491ac9860", + "uuid": "32bc567b-075f-4471-a601-38f7694f1c35", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3053,7 +3053,7 @@ "observation_count": 1 }, { - "uuid": "2270ead6-e4ca-4562-9401-7e2a90fb12c7", + "uuid": "83a60dd2-560a-4f92-9613-83507b73133c", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3083,7 +3083,7 @@ "observation_count": 1 }, { - "uuid": "e15826d8-2e05-4e31-ad4b-0dafd26378eb", + "uuid": "34714371-9105-4e7b-93dd-6f7190d15e55", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3113,7 +3113,7 @@ "observation_count": 1 }, { - "uuid": "2783a60d-fa4d-4790-8d33-8c9e8d8cc90f", + "uuid": "acfecec9-e491-441e-8df3-02cd52b61191", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3143,7 +3143,7 @@ "observation_count": 1 }, { - "uuid": "f0a1bac8-592d-4c9b-ac1f-fe9c1625f025", + "uuid": "34e71c1b-05c8-48dd-8625-ddadea1b2445", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3173,7 +3173,7 @@ "observation_count": 1 }, { - "uuid": "0eebf0eb-0a9e-4807-8eba-db432a0f7c53", + "uuid": "94d5fcdc-3df6-4b46-bcd0-594dfc907488", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3203,7 +3203,7 @@ "observation_count": 1 }, { - "uuid": "519c4f40-16e7-4a05-8264-98ed96cfb991", + "uuid": "f80db1ec-1739-496f-a6f9-47f3c18b280d", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3233,7 +3233,7 @@ "observation_count": 1 }, { - "uuid": "06abba46-1ae9-4f1d-a2ff-d44eace85eee", + "uuid": "5ef9bdf6-62bc-4f50-8a7e-119a1c2f3aad", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3263,7 +3263,7 @@ "observation_count": 1 }, { - "uuid": "8f5321ad-8b7f-47e5-8f3e-910bbaf8f893", + "uuid": "a90c5f52-5bcf-4920-8b32-c2c236b7ae28", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3293,7 +3293,7 @@ "observation_count": 1 }, { - "uuid": "205d818e-5803-46e7-bd76-f46fb579dbd2", + "uuid": "12413650-f9e2-4308-93c7-886ce4f3956e", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3323,7 +3323,7 @@ "observation_count": 1 }, { - "uuid": "5d57bcc2-286e-4545-bbbd-9c8c04fde6ae", + "uuid": "dc083c88-6ad5-4b84-b7f1-b890ff4e8561", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3353,7 +3353,7 @@ "observation_count": 1 }, { - "uuid": "9db5f35a-070a-4a98-b180-978663367e75", + "uuid": "0189b8ff-f12e-40d1-8d8f-3dc7a5e1bffa", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3383,7 +3383,7 @@ "observation_count": 1 }, { - "uuid": "9fdbde0b-db7f-4405-ae40-2a14108b8e88", + "uuid": "6362a941-a7a3-422a-970f-d650c91d18ec", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3413,7 +3413,7 @@ "observation_count": 1 }, { - "uuid": "3316dfa0-edd9-4f5f-921b-bbf7cec86b3e", + "uuid": "3bab127f-53fe-4a60-a1f0-c6efaaa7dbe2", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3443,7 +3443,7 @@ "observation_count": 1 }, { - "uuid": "8cec1cee-8967-4833-9f92-6fe8320e186c", + "uuid": "545502ce-7fa4-44e3-bc00-411f93df5875", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3473,7 +3473,7 @@ "observation_count": 1 }, { - "uuid": "e4975798-4e8e-40f9-a00b-2aaf34bb9094", + "uuid": "75a0857b-f4da-4e5d-8000-33b3c8b8ed6e", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3503,7 +3503,7 @@ "observation_count": 1 }, { - "uuid": "5cb38459-b88c-4108-9eec-38cbc1f08abe", + "uuid": "c3fc0369-f22e-488d-b5b7-9c75e0de4b1c", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3533,7 +3533,7 @@ "observation_count": 1 }, { - "uuid": "c34d47f1-1f5d-4fea-a5e4-6fe6b73d3aaa", + "uuid": "e52a49f4-79f6-4c0f-b06d-7c52e788794f", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3563,7 +3563,7 @@ "observation_count": 1 }, { - "uuid": "e867140c-7a90-434b-916c-45dbed98b58f", + "uuid": "6e782a95-0fee-4f5a-b802-f061ef719fd2", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3593,7 +3593,7 @@ "observation_count": 1 }, { - "uuid": "406d950c-02d9-4290-b048-bd77e08b2d4d", + "uuid": "2247c3f5-2393-4d87-a46b-5160cd80ad26", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3623,7 +3623,7 @@ "observation_count": 1 }, { - "uuid": "2aaa7a6d-2a07-439c-be44-bc84c9cc94f0", + "uuid": "4642db20-cf61-4d04-891d-7d9933c6721e", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3653,7 +3653,7 @@ "observation_count": 1 }, { - "uuid": "f6136a80-22a1-4ea9-94b7-d5f845818da9", + "uuid": "8022c7e2-4760-40ae-9621-6d1d52d06acc", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3683,7 +3683,7 @@ "observation_count": 1 }, { - "uuid": "e281c4d7-10f4-48c7-9ad4-8cbe6ac083df", + "uuid": "81f1dc85-41d3-4d12-86cb-6a93338fb4c8", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3713,7 +3713,7 @@ "observation_count": 1 }, { - "uuid": "c7dd3197-99d8-48e0-a40c-e5a1fad5d6e7", + "uuid": "cb4e42fd-c331-4da7-9341-4e546b9ce8cd", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3743,7 +3743,7 @@ "observation_count": 1 }, { - "uuid": "167dc6a7-13d2-4a30-a374-d201bd27eeda", + "uuid": "ad8be026-4531-46e4-b02d-43d27f93c948", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3773,7 +3773,7 @@ "observation_count": 1 }, { - "uuid": "1e8abd87-aa84-4b07-ab72-2e3ca543df47", + "uuid": "c92381ef-2e2b-493b-ada9-6e9fbd3d2c2d", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3803,7 +3803,7 @@ "observation_count": 1 }, { - "uuid": "6288100f-0dc0-486d-8c47-a5f59422468d", + "uuid": "b5bb3650-78cb-435b-bb66-7f04e38bf470", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3833,7 +3833,7 @@ "observation_count": 1 }, { - "uuid": "8466fe18-e7f0-4649-9fb6-fbe5c679b330", + "uuid": "7ab34230-19e8-4543-999f-2910c1173be5", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3863,7 +3863,7 @@ "observation_count": 1 }, { - "uuid": "f2464c58-e3df-4c6d-bd8d-2224397e71e1", + "uuid": "2834f2ee-480f-476b-8ae6-5f8975cf493b", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3893,7 +3893,7 @@ "observation_count": 1 }, { - "uuid": "0cfce5bb-e258-4461-941b-1bd22b702a7a", + "uuid": "bb079952-9000-48fd-84da-0e4f38f28964", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3923,7 +3923,7 @@ "observation_count": 1 }, { - "uuid": "653fc709-1048-41aa-92f9-478af77a7c37", + "uuid": "9fa7ec57-8aad-4680-a867-d6e07a1fde4d", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3953,7 +3953,7 @@ "observation_count": 1 }, { - "uuid": "b7b9352f-2ec2-442b-8cdf-91d4bee18834", + "uuid": "21b36dca-9579-4a20-a947-beaa7afb6b7a", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -3983,7 +3983,7 @@ "observation_count": 1 }, { - "uuid": "36bf955e-147b-4a45-9a05-013ea643a982", + "uuid": "a53e930e-3042-4967-ad17-888f29e653af", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -4013,7 +4013,7 @@ "observation_count": 1 }, { - "uuid": "2d590ffd-f6cc-4ded-a836-66e51c06846a", + "uuid": "e89d9485-e437-40c3-987e-be079dbc45eb", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -4043,7 +4043,7 @@ "observation_count": 1 }, { - "uuid": "93cd55b3-4ac4-40a5-a894-2414b57d0b63", + "uuid": "bc31a283-a414-4446-87cb-48d9c8a3db28", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -4073,7 +4073,7 @@ "observation_count": 1 }, { - "uuid": "dd1b0426-a9a7-4947-8ccb-d725d5f705af", + "uuid": "9b679b84-0d9f-44fd-86dd-3556bd131e42", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -4103,7 +4103,7 @@ "observation_count": 1 }, { - "uuid": "88f0affd-b9e9-4e25-a5a1-75c8231e1851", + "uuid": "4b6ceb75-7ede-4ec3-a1bc-de4bdd8f0dca", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -4133,7 +4133,7 @@ "observation_count": 1 }, { - "uuid": "e64ace78-5211-42c7-b398-febe15bd72a9", + "uuid": "6c9ddf7b-f037-4482-89fc-e45728169d88", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -4163,7 +4163,7 @@ "observation_count": 1 }, { - "uuid": "883bf5c9-2f23-46d7-ad2f-1735bb07da20", + "uuid": "249143e2-bbd8-46a6-96a8-673dfbf7e14f", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -4193,7 +4193,7 @@ "observation_count": 1 }, { - "uuid": "70bd86dd-db63-43b9-bd4d-d3897c323fe4", + "uuid": "25099a0a-db82-4354-aa16-dc0e7d0ee301", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -4223,7 +4223,7 @@ "observation_count": 1 }, { - "uuid": "b5b9c3d2-e8d5-4217-9f1d-b10b583cd296", + "uuid": "1435b3de-9953-4cb5-acd8-ace0df2570ad", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -4253,7 +4253,7 @@ "observation_count": 1 }, { - "uuid": "de6c7aca-f3fd-4ff3-953f-da914f079995", + "uuid": "28747a0a-dd14-414c-b193-514208ba0a05", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -4283,7 +4283,7 @@ "observation_count": 1 }, { - "uuid": "1ee101ec-a6ce-4a5d-85f2-bd0cf3799333", + "uuid": "39d52b69-8066-4028-81de-a1b938cf0630", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -4313,7 +4313,7 @@ "observation_count": 1 }, { - "uuid": "f0303e9e-dfcb-4d55-95fa-0db98a6e8dab", + "uuid": "6dcd0d3f-5ed9-49d4-93fc-b9ec63c9f699", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -4343,7 +4343,7 @@ "observation_count": 1 }, { - "uuid": "8105b4c6-62ba-469a-bdd7-5478df1c8984", + "uuid": "62000494-b4fe-4a7d-b0d2-013b911e7884", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -4373,7 +4373,7 @@ "observation_count": 1 }, { - "uuid": "04540192-c9e8-45ff-bf3b-bd2038e41635", + "uuid": "b9d02bc9-68e5-4900-b30e-763f6d760a70", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -4403,7 +4403,7 @@ "observation_count": 1 }, { - "uuid": "10797cc6-e647-4cae-a16f-168869ac91cd", + "uuid": "c562b4b3-09ce-45db-b355-cd6c9d412f18", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -4433,7 +4433,7 @@ "observation_count": 1 }, { - "uuid": "a7bf6571-716a-4d8f-8f3b-070bd4a1b975", + "uuid": "55cbe6f5-3aad-473a-8a3d-552b2525eb5f", "concept": "fns.snap.total_payment_error_rate", "family_patterns": [ "fns.snap.total_payment_error_rate" @@ -4463,7 +4463,7 @@ "observation_count": 1 }, { - "uuid": "e07881a2-39b4-416f-8cd9-c503b1fdad4a", + "uuid": "748670af-cafa-416e-afcd-b2c303593782", "concept": "fns.snap.total_persons", "family_patterns": [ "fns.snap.total_persons" @@ -4481,7 +4481,7 @@ "observation_count": 0 }, { - "uuid": "d4710c2c-cdb9-49ff-aa96-3bfb53429dbb", + "uuid": "3fa6210a-33a1-4a3e-bbe5-2af34f29a2b9", "concept": "fns.snap.underpayment_error_rate", "family_patterns": [ "fns.snap.underpayment_error_rate" @@ -4511,7 +4511,7 @@ "observation_count": 1 }, { - "uuid": "37d08842-a1c9-40f3-b8dd-8fe010906a9d", + "uuid": "5a76270d-3418-49c8-b510-a08c0df0fc8c", "concept": "fns.wic.total_participation", "family_patterns": [ "fns.wic.total_participation" @@ -4529,7 +4529,7 @@ "observation_count": 0 }, { - "uuid": "10b01c7f-95e4-498d-a832-486ed61b1c51", + "uuid": "a3e8d1fb-5f29-40a8-ab14-a06a53ecb6af", "concept": "nbb.business_barometer.overall", "family_patterns": [ "nbb.business_barometer.overall" @@ -4551,7 +4551,7 @@ "observation_count": 0 }, { - "uuid": "5e7f88da-8f50-4e0e-9b7b-913da0b9d53e", + "uuid": "64bd2834-d559-4bf2-abff-3eae6fcf6b4b", "concept": "nbb.consumer_confidence.indicator", "family_patterns": [ "nbb.consumer_confidence.indicator" @@ -4573,7 +4573,7 @@ "observation_count": 0 }, { - "uuid": "bd245ca4-e8b7-45e5-b998-72601a3d75fb", + "uuid": "03a9d380-e5cf-45d8-b4d8-b3da28aad043", "concept": "nbb.gdp.flash_qoq", "family_patterns": [ "nbb.gdp.flash_qoq" @@ -4595,7 +4595,7 @@ "observation_count": 0 }, { - "uuid": "9dc148f2-844f-438f-94a6-1ad7197c9fa1", + "uuid": "8436c65e-a9c6-46db-a2b5-77d2cdfff5c2", "concept": "ons.cpi.annual_rate", "family_patterns": [ "ons.cpi.annual_rate.{P}" @@ -4627,7 +4627,7 @@ "observation_count": 1 }, { - "uuid": "bb244193-568a-465b-ae0b-bdae1fb23ae8", + "uuid": "c7d9ec49-df82-41fa-a184-5cb76379a57e", "concept": "ons.cpih.annual_rate", "family_patterns": [ "ons.cpih.annual_rate.{P}" @@ -4659,7 +4659,7 @@ "observation_count": 1 }, { - "uuid": "0ccc9fc8-6ad9-44d6-a406-a13c0c6b779e", + "uuid": "780bdbec-6c59-4714-826f-a3dc66f3b6ab", "concept": "ons.gdp.monthly_growth", "family_patterns": [ "ons.gdp.monthly_growth.{P}" @@ -4691,7 +4691,7 @@ "observation_count": 1 }, { - "uuid": "5394d93b-56a4-47fd-89e3-d0706df4ee7e", + "uuid": "b608f8f5-08be-4535-bae0-98282561532e", "concept": "ons.hmrc.paye_payrolled_employees", "family_patterns": [ "ons.hmrc.paye_payrolled_employees.{P}" @@ -4723,7 +4723,7 @@ "observation_count": 1 }, { - "uuid": "e58098cf-6320-4028-9b00-9fdfb4a8f9dc", + "uuid": "15c5a134-1cbe-4990-9efa-97702cd3d4de", "concept": "ons.labour.unemployment_rate", "family_patterns": [ "ons.labour.unemployment_rate.{P}" @@ -4755,7 +4755,7 @@ "observation_count": 1 }, { - "uuid": "7c35b6d4-a697-40d8-9c66-b8809728b54d", + "uuid": "c5d4619d-a81e-457b-b970-b2e97145caf9", "concept": "ons.pusf.j5ii.public_sector_net_borrowing_ex_banks", "family_patterns": [ "ons.pusf.j5ii.public_sector_net_borrowing_ex_banks.{P}" @@ -4788,7 +4788,7 @@ "observation_count": 1 }, { - "uuid": "27924b23-60c0-4a61-aadc-8a2a2889b4ec", + "uuid": "35138dc0-68d5-40c8-abc8-b1a97789fffa", "concept": "ons.retail_sales.volume_mom", "family_patterns": [ "ons.retail_sales.volume_mom.{P}" @@ -4820,7 +4820,7 @@ "observation_count": 1 }, { - "uuid": "b0ba0014-50a8-46b0-9549-d51d03a281d3", + "uuid": "d79b97a3-405d-4ac5-a47b-7733cd0ac673", "concept": "rba.cash_rate_target", "family_patterns": [ "rba.cash_rate_target.{P}" @@ -4852,7 +4852,7 @@ "observation_count": 1 }, { - "uuid": "b311da3b-4e74-4c77-955c-b40673658591", + "uuid": "b908460f-bc4c-4416-a7d6-75b43244b752", "concept": "ssa.ssi.total_recipients", "family_patterns": [ "ssa.ssi.total_recipients" @@ -4870,7 +4870,7 @@ "observation_count": 0 }, { - "uuid": "9069a65e-39d1-4735-99f3-553e6e90be7a", + "uuid": "99a7178c-5085-4ec2-9714-c4d03f10f184", "concept": "statbel.cpi.headline_yoy", "family_patterns": [ "statbel.cpi.headline_yoy" @@ -4892,7 +4892,7 @@ "observation_count": 0 }, { - "uuid": "4744d1f0-7fe3-4dcc-8b93-e8b46af4a0f7", + "uuid": "8fc8eb86-6f04-4c6b-91ae-ca2a490a175f", "concept": "statbel.health_index.yoy", "family_patterns": [ "statbel.health_index.yoy" @@ -4914,7 +4914,7 @@ "observation_count": 0 }, { - "uuid": "f1330368-0ba5-4649-94a3-50400bd50154", + "uuid": "987c1860-5642-494e-b9da-4034691430e5", "concept": "statcan.36-10-0434-01.all_industries.month_to_month_percent_change", "family_patterns": [ "statcan.36-10-0434-01.all_industries.month_to_month_percent_change" @@ -4946,7 +4946,7 @@ "observation_count": 1 }, { - "uuid": "72f752bb-57a5-4556-875c-a6c14db3d0fa", + "uuid": "dcff2e47-6562-406e-95ac-7914478803e1", "concept": "statcan.building_permits.total_value_mom.canada", "family_patterns": [ "statcan.building_permits.total_value_mom.canada.{P}" @@ -4978,7 +4978,7 @@ "observation_count": 1 }, { - "uuid": "1cc75c76-34d2-47d6-ac69-db9a5e75b16f", + "uuid": "51be759e-6118-4e1c-bef0-823e40fd004b", "concept": "statcan.cpi.all_items_annual_rate.canada", "family_patterns": [ "statcan.cpi.all_items_annual_rate.canada.{P}" @@ -5011,7 +5011,7 @@ "observation_count": 1 }, { - "uuid": "182061a6-d331-4649-83c8-a4252139247f", + "uuid": "23bf4da6-874a-4b29-affb-ee68d03d2dee", "concept": "statcan.cpi.allitems.yoy", "family_patterns": [ "statcan.cpi.allitems.yoy.{P}" @@ -5044,7 +5044,7 @@ "observation_count": 1 }, { - "uuid": "8fc52914-0d14-4d2f-a74b-5f5edfd9aeee", + "uuid": "52fba74f-ea83-4b12-981f-3ca2c581686d", "concept": "statcan.employment_insurance.regular_beneficiaries.canada", "family_patterns": [ "statcan.employment_insurance.regular_beneficiaries.canada.{P}" @@ -5077,7 +5077,7 @@ "observation_count": 1 }, { - "uuid": "aaf2c099-57ef-4978-a0c1-5a3d93a355cf", + "uuid": "78d00eef-fa0e-4248-ae18-eef6536568d4", "concept": "statcan.employment_insurance.regular_beneficiaries.canada", "family_patterns": [ "statcan.employment_insurance.regular_beneficiaries.canada.{P}" @@ -5110,7 +5110,7 @@ "observation_count": 1 }, { - "uuid": "37a2108c-fc20-4773-8955-1fa673d4ea39", + "uuid": "166d1a5a-b627-4d1f-9f79-682f28692dd5", "concept": "statcan.gdp_by_industry.monthly_growth", "family_patterns": [ "statcan.gdp_by_industry.monthly_growth.{P}" @@ -5143,7 +5143,7 @@ "observation_count": 1 }, { - "uuid": "eefd1b57-4894-4b89-a207-854dd60f3a27", + "uuid": "552c1dda-abe3-4f6c-86fb-473f455b5e15", "concept": "statcan.lfs.employment_change", "family_patterns": [ "statcan.lfs.employment_change" @@ -5173,7 +5173,7 @@ "observation_count": 1 }, { - "uuid": "9c699030-1f40-4bd3-baca-b84a13ebf54e", + "uuid": "184e9ddf-aeeb-47eb-9ea4-a60eafa878cc", "concept": "statcan.lfs.unemployment_rate", "family_patterns": [ "statcan.lfs.unemployment_rate" @@ -5203,7 +5203,7 @@ "observation_count": 1 }, { - "uuid": "1404f983-f6b2-4f65-aa64-cfcbe00f3718", + "uuid": "42b102d3-0883-4935-843d-1a8e80434f49", "concept": "statcan.retail_trade.sales_mom.canada", "family_patterns": [ "statcan.retail_trade.sales_mom.canada.{P}" @@ -5236,7 +5236,7 @@ "observation_count": 1 }, { - "uuid": "0c1046ce-6fd7-420b-93e2-77b9800ff91a", + "uuid": "057c368b-a4e8-42bc-ab29-5b55f4eadfd5", "concept": "statcan.wholesale_trade.sales_mom_exclusions.canada", "family_patterns": [ "statcan.wholesale_trade.sales_mom_exclusions.canada.{P}" @@ -5269,7 +5269,7 @@ "observation_count": 1 }, { - "uuid": "ed14fae9-fe4c-4f4c-bd85-c4df6df4b05f", + "uuid": "37ef2549-b8d4-4020-8ebd-a2ceb5fe8401", "concept": "statjp.cpi.all_items_annual_rate.japan", "family_patterns": [ "statjp.cpi.all_items_annual_rate.japan.{P}" @@ -5302,7 +5302,7 @@ "observation_count": 1 }, { - "uuid": "fa8946a8-e52a-4a62-a75f-9facf4117d84", + "uuid": "d26a3e29-c04f-4c86-825d-fba2d306e7af", "concept": "statjp.cpi.tokyo_all_items_annual_rate", "family_patterns": [ "statjp.cpi.tokyo_all_items_annual_rate.{P}" @@ -5335,7 +5335,7 @@ "observation_count": 1 }, { - "uuid": "e2312b47-b581-445c-867c-71bd50123492", + "uuid": "62e78be0-309e-49ba-8604-67abb80f1a03", "concept": "statjp.cpi.tokyo_all_items_yoy", "family_patterns": [ "statjp.cpi.tokyo_all_items_yoy" @@ -5353,7 +5353,7 @@ "observation_count": 0 }, { - "uuid": "ec446a59-3ea0-4dc1-804b-b6a40506d6fa", + "uuid": "f3c0a92a-1af7-4afc-8578-7ac55076227d", "concept": "statjp.household_spending.real_yoy.two_or_more_person_households", "family_patterns": [ "statjp.household_spending.real_yoy.two_or_more_person_households.{P}" @@ -5386,7 +5386,7 @@ "observation_count": 1 }, { - "uuid": "09e40854-6b14-4128-9c54-719192ba9eae", + "uuid": "d15b52c7-e453-4276-bf9f-0b3362a37ecf", "concept": "statjp.lfs.unemployment_rate.japan", "family_patterns": [ "statjp.lfs.unemployment_rate.japan.{P}" @@ -5419,7 +5419,7 @@ "observation_count": 1 }, { - "uuid": "dc7d23ca-63ab-40f1-affa-213cf3880dde", + "uuid": "f471eabf-fcff-449b-bac7-60a8561b063b", "concept": "treasury.mts.monthly_deficit", "family_patterns": [ "treasury.mts.monthly_deficit.{P}" @@ -5451,7 +5451,7 @@ "observation_count": 1 }, { - "uuid": "d37c0017-e5d6-4b7e-afc6-3cc3322bec38", + "uuid": "5d0b93c4-1c82-4598-869d-010d56860983", "concept": "us.bea.core_pce.mom_sa", "family_patterns": [ "us.bea.core_pce.mom_sa.{P}" @@ -5485,7 +5485,7 @@ "observation_count": 2 }, { - "uuid": "d085b526-5557-422f-b623-296d7152cbe0", + "uuid": "d542cb8d-46ef-4af5-b7e0-8d38574120c2", "concept": "us.census.housing_starts.total_saar", "family_patterns": [ "us.census.housing_starts.total_saar.{P}" @@ -5518,7 +5518,7 @@ "observation_count": 1 }, { - "uuid": "cf51edfa-cbc0-4d2d-ade1-32a1240da80f", + "uuid": "1ad67747-d9d8-4ff9-9ebc-58206e658518", "concept": "us.dol.initial_claims.sa", "family_patterns": [ "us.dol.initial_claims.sa" @@ -5550,7 +5550,7 @@ "observation_count": 5 }, { - "uuid": "1b5eec21-e424-44fe-a922-d99929901585", + "uuid": "830e68bb-9ed6-493c-a8a4-440ba8e53f76", "concept": "us.dol.initial_claims.sa", "family_patterns": [ "us.dol.initial_claims.sa.{P}" @@ -5583,7 +5583,7 @@ "observation_count": 1 }, { - "uuid": "5b0068e3-675a-4c77-90a7-1bcfdc69f11d", + "uuid": "a4729a07-d9cd-4736-9473-2f845fd71a2d", "concept": "us.fed.fomc.target_range_upper", "family_patterns": [ "us.fed.fomc.target_range_upper.{P}" @@ -5616,7 +5616,7 @@ "observation_count": 1 }, { - "uuid": "f7b45e6f-5085-4bb0-9645-cf0f915f0cae", + "uuid": "b5a74252-bf95-488c-81f6-3deab496c477", "concept": "us.frb.industrial_production.total.mom_sa", "family_patterns": [ "us.frb.industrial_production.total.mom_sa.{P}" @@ -5649,7 +5649,7 @@ "observation_count": 1 }, { - "uuid": "f0b3ebdd-f2b2-4688-b8b4-325bcd9485c9", + "uuid": "8146ecb0-3ef3-49eb-b8a0-e61d15aa276e", "concept": "usaspending.dod.new_prime_awards", "family_patterns": [ "usaspending.dod.new_prime_awards" @@ -5667,7 +5667,7 @@ "observation_count": 0 }, { - "uuid": "b842fec2-c251-48a9-9aea-7ad64113ee55", + "uuid": "a2649fd9-f382-4003-ba70-01030bc93b5e", "concept": "usaspending.dod.prime_award_obligations", "family_patterns": [ "usaspending.dod.prime_award_obligations" @@ -5685,7 +5685,7 @@ "observation_count": 0 }, { - "uuid": "9154e0d2-eff3-4530-af75-b363d6d88809", + "uuid": "82ff2d7f-04ee-4366-8fe4-b8e919e6efa6", "concept": "usaspending.dod.prime_award_transactions", "family_patterns": [ "usaspending.dod.prime_award_transactions" @@ -5703,7 +5703,7 @@ "observation_count": 0 }, { - "uuid": "dde380c6-2c4f-402c-ae36-e1f6f8c4da2c", + "uuid": "11ea0d3d-fab9-42bb-b679-3471d4b60323", "concept": "usaspending.dod.prime_contract_obligations", "family_patterns": [ "usaspending.dod.prime_contract_obligations" @@ -5721,7 +5721,7 @@ "observation_count": 0 }, { - "uuid": "f9cf8a2a-dddc-4086-8ee0-12d7aa97e6c6", + "uuid": "91e3b668-cb4b-4682-94a5-7ae404a6565d", "concept": "usaspending.dod.small_business_contract_obligation_share", "family_patterns": [ "usaspending.dod.small_business_contract_obligation_share" @@ -5739,7 +5739,7 @@ "observation_count": 0 }, { - "uuid": "e00e27d8-c584-4310-ab1a-3f2dce010041", + "uuid": "96d17026-b54c-4409-98c9-339b6725fde7", "concept": "usaspending.dod.unique_prime_contract_recipients", "family_patterns": [ "usaspending.dod.unique_prime_contract_recipients" @@ -5757,7 +5757,7 @@ "observation_count": 0 }, { - "uuid": "07209de1-43ea-4728-985b-948c9a6eb3e1", + "uuid": "4266e63f-dbf3-4c86-be8d-44315dbe32e3", "concept": "usda.fsa.crp.enrolled_acres_total", "family_patterns": [ "usda.fsa.crp.enrolled_acres_total" diff --git a/scripts/build_series_catalog.py b/scripts/build_series_catalog.py index 5af1220..c5f081c 100644 --- a/scripts/build_series_catalog.py +++ b/scripts/build_series_catalog.py @@ -317,13 +317,60 @@ def build_catalog( rows = [json.loads(line) for line in raw.decode().splitlines() if line.strip()] identities = build_identities(rows) + # Canonicalize each observed bucket through the existing catalog: a + # curated alias or a unique prior concept/alias hit keeps BOTH the prior + # UUID and the prior canonical concept (curation owns naming; observed + # spellings become aliases). Buckets landing on the same canonical + # identity merge. + canonical: dict[tuple[str, str, str], dict] = {} + for key in sorted(identities): + ident = identities[key] + concept, geo_key, entity_key = key + names = ident["concepts"] | ident["source_concepts"] | {concept} + prior = existing.match(key, names) + canon_concept = prior["concept"] if prior else concept + canon_key = (canon_concept, geo_key, entity_key) + bucket = canonical.setdefault( + canon_key, + { + "prior": prior, + "patterns": set(), + "concepts": set(), + "source_concepts": set(), + "rid_patterns": set(), + "suspects": set(), + "units": Counter(), + "period_types": Counter(), + "geography": ident["geography"], + "entity": ident["entity"], + "sources": set(), + "period_values": [], + "count": 0, + }, + ) + if bucket["prior"] is None: + bucket["prior"] = prior + elif prior is not None and prior["uuid"] != bucket["prior"]["uuid"]: + raise SystemExit( + f"identity {canon_key} inherits two different UUIDs " + f"({bucket['prior']['uuid']}, {prior['uuid']}) — curate the " + "existing catalog before regenerating" + ) + bucket["patterns"] |= ident["patterns"] + bucket["concepts"] |= ident["concepts"] + bucket["source_concepts"] |= ident["source_concepts"] + bucket["rid_patterns"] |= ident["rid_patterns"] + bucket["suspects"] |= ident["suspects"] + bucket["units"] += ident["units"] + bucket["period_types"] += ident["period_types"] + bucket["sources"] |= ident["sources"] + bucket["period_values"] += ident["period_values"] + bucket["count"] += ident["count"] + series: list[dict] = [] used_uuids: dict[str, tuple] = {} - def mint(key: tuple, names: set[str]) -> tuple[str, list[str]]: - prior = existing.match(key, names) - row_uuid = prior["uuid"] if prior else str(uuid_module.uuid4()) - curated_aliases = list(prior.get("aliases", [])) if prior else [] + def claim_uuid(row_uuid: str, key: tuple) -> str: if row_uuid in used_uuids: raise SystemExit( f"UUID collision: {row_uuid} claimed by both " @@ -331,34 +378,37 @@ def mint(key: tuple, names: set[str]) -> tuple[str, list[str]]: "catalog before regenerating" ) used_uuids[row_uuid] = key - return row_uuid, curated_aliases + return row_uuid all_suspects: set[str] = set() - for key in sorted(identities): - ident = identities[key] - concept, _, _ = key - names = ident["concepts"] | ident["source_concepts"] | {concept} - row_uuid, curated_aliases = mint(key, names) + for canon_key in sorted(canonical): + bucket = canonical[canon_key] + concept, _, _ = canon_key + prior = bucket["prior"] + row_uuid = claim_uuid( + prior["uuid"] if prior else str(uuid_module.uuid4()), canon_key + ) + curated_aliases = set(prior.get("aliases", [])) if prior else set() aliases = sorted( - (ident["concepts"] | ident["source_concepts"] | set(curated_aliases)) + (bucket["concepts"] | bucket["source_concepts"] | curated_aliases) - {concept} ) - all_suspects.update(ident["suspects"]) + all_suspects.update(bucket["suspects"]) series.append({ "uuid": row_uuid, "concept": concept, - "family_patterns": sorted(ident["patterns"]), + "family_patterns": sorted(bucket["patterns"]), "status": "observed", - "unit": _sole(ident["units"], "unit", key), - "cadence": _sole(ident["period_types"], "cadence", key), - "geography": ident["geography"], - "entity": ident["entity"], - "sources": sorted(ident["sources"]), + "unit": _sole(bucket["units"], "unit", canon_key), + "cadence": _sole(bucket["period_types"], "cadence", canon_key), + "geography": bucket["geography"], + "entity": bucket["entity"], + "sources": sorted(bucket["sources"]), "aliases": aliases, - "rid_patterns": sorted(ident["rid_patterns"]), - "first_observed_period": min(ident["period_values"], default=None), - "last_observed_period": max(ident["period_values"], default=None), - "observation_count": ident["count"], + "rid_patterns": sorted(bucket["rid_patterns"]), + "first_observed_period": min(bucket["period_values"], default=None), + "last_observed_period": max(bucket["period_values"], default=None), + "observation_count": bucket["count"], }) docket_raw = b"" @@ -398,7 +448,11 @@ def mint(key: tuple, names: set[str]) -> tuple[str, list[str]]: "no geography mapping; extend COUNTRY_GEOGRAPHY" ) key = (concept, _geo_key(geography), _entity_key(None)) - row_uuid, curated_aliases = mint(key, {concept}) + prior = existing.match(key, {concept}) + row_uuid = claim_uuid( + prior["uuid"] if prior else str(uuid_module.uuid4()), key + ) + curated_aliases = set(prior.get("aliases", [])) if prior else set() row = { "uuid": row_uuid, "concept": concept, From 3cb0c8a0750a3d734e8ccc1df79abab25cad204b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 1 Aug 2026 13:41:45 -0400 Subject: [PATCH 04/11] Series catalog v3: append-only UUID registry as identity authority Answers the second adversarial review (BLOCK) point by point: 1. Identity continuity is now checked against a prior committed registry, not the catalog itself: ledger/series_uuid_registry.jsonl is an append-only minting ledger (one line per binding; supersedes chains per identity). The builder inherits UUIDs from the registry, --check proves catalog/registry agreement plus registry append-only vs git HEAD, and CI proves append-only vs the PR base. All 201 committed UUIDs are preserved (bootstrap = the 2859ecb catalog's bindings, 201/201 carried forward). A write that would change or drop an existing identity's UUID refuses without --allow-remint --remint-note, which records a supersede event. 2. Concept/alias inheritance is scoped to the same (geography, entity); the only exception is docket-placeholder enrichment (docket-only row, entity unknown, geography absent or equal on level/id). 3. measure.source_concept values are provenance (new per-row source_concepts field), never aliases and never identity. The two docket links that machine labels carried (BLS AHE private, StatCan EI beneficiaries) are now explicit curated aliases. 4. UUID uniqueness keys on the parsed 128-bit value and canonical lowercase text is required everywhere (catalog and registry). 5. A missing docket seed is a hard error in every mode. 6. Geography boundary vintage joins the identity key per the fact-identity ADR. 7. Date-shaped segments strip only when they denote the row's own period (direct spelling or overlapping calendar window); disjoint or impossible tokens stay in the identity and are flagged as suspects. Also fixes a latent v2 bug where MONTHS_ABBREV carried 13 entries, shifting derived abbreviations for October-December. Regenerated artifact: 201 series (155 observed, 46 docket-only), suspects=0, ambiguous_aliases=0, minted=0, superseded=0. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 21 + ledger/series_catalog.json | 619 ++++++++++++++--- ledger/series_uuid_registry.jsonl | 201 ++++++ scripts/build_series_catalog.py | 1022 +++++++++++++++++++++++----- tests/test_build_series_catalog.py | 564 +++++++++++++-- 5 files changed, 2117 insertions(+), 310 deletions(-) create mode 100644 ledger/series_uuid_registry.jsonl diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2ceda54..16ab683 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,6 +39,27 @@ jobs: uv run --locked python scripts/build_series_catalog.py --check uv run --locked pytest tests/test_build_series_catalog.py -q + # The UUID registry is the append-only minting ledger behind the + # series catalog: a PR may extend it, never edit or truncate it. + # --check already proves catalog/registry agreement and (via git) + # that the working tree extends HEAD; this step closes the + # cross-commit hole by proving the PR head extends the PR BASE. + - name: Series UUID registry append-only vs base + if: github.event_name == 'pull_request' + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + git fetch --no-tags --depth=1 origin "$BASE_SHA" + if git cat-file -e "$BASE_SHA:ledger/series_uuid_registry.jsonl" \ + 2>/dev/null; then + git show "$BASE_SHA:ledger/series_uuid_registry.jsonl" \ + > /tmp/base-series-uuid-registry.jsonl + else + : > /tmp/base-series-uuid-registry.jsonl + fi + uv run --locked python scripts/build_series_catalog.py \ + --verify-registry-append-only /tmp/base-series-uuid-registry.jsonl + - name: Lint Arch surface run: > uv run ruff check diff --git a/ledger/series_catalog.json b/ledger/series_catalog.json index e7799c9..815ca19 100644 --- a/ledger/series_catalog.json +++ b/ledger/series_catalog.json @@ -1,18 +1,12 @@ { - "comment": "Canonical series catalog. One row per (concept, geography, entity) identity; uuid is minted once and never re-minted (regeneration preserves it by identity, then by concept/alias). Consumers reference series by uuid or concept only. Regenerate with scripts/build_series_catalog.py; verify with --check. Cross-spelling and cross-vintage merges are manual curation: keep the surviving row's uuid, move absorbed spellings to aliases — curated aliases persist across regeneration. Aliases listed in ambiguous_aliases match multiple rows and never drive identity inheritance.", - "generator_version": 2, + "comment": "Canonical series catalog. One row per (concept, geography level/id/vintage, entity) identity. UUID authority is the append-only ledger/series_uuid_registry.jsonl (digest below): a uuid is minted once, inherited from the registry on every regeneration, and changes only through an explicit --allow-remint supersede event recorded there. Consumers reference series by uuid or concept only. Regenerate with scripts/build_series_catalog.py; verify with --check. Aliases are curated identity statements (plus observed spellings of the same identity) and inherit only within one (geography, entity) slice; aliases listed in ambiguous_aliases match multiple rows and never drive inheritance. source_concepts are publisher labels — provenance, never identity. Cross-spelling and cross-vintage merges are manual curation: delete the absorbed row, alias its concept on the survivor, regenerate with --allow-remint.", + "generator_version": 3, "observations_sha256": "63127ff427a4aa3884f54dd1ee070ab631c15ebb38f23fdc9095a45e9204109f", "observation_rows": 168, "docket_seed_sha256": "930424fb48c0be4c9e2ce17d4e0f2a6be886408e814c80324174a7a303fa0271", + "uuid_registry_sha256": "c4ccda3f1746ff06cf8cc17dc221ea3a82b7b16a5d719361e2025bc5d7b63356", "suspect_segments": [], - "ambiguous_aliases": [ - "CPI/3.10001.10.50.M", - "JTSJOL", - "PCEPILFE", - "prc_hicp_minr/M.RCH_A.TOTAL.EA21", - "v41690973", - "v65201210" - ], + "ambiguous_aliases": [], "series": [ { "uuid": "efbb2901-f8f8-4f5b-9835-56e5cc3e62ff", @@ -37,7 +31,9 @@ "abs" ], "aliases": [ - "abs.building_approvals.total_dwellings_mom.australia.may_2026", + "abs.building_approvals.total_dwellings_mom.australia.may_2026" + ], + "source_concepts": [ "building-approvals-australia release page" ], "rid_patterns": [ @@ -69,7 +65,8 @@ "sources": [ "abs" ], - "aliases": [ + "aliases": [], + "source_concepts": [ "CPI/3.10001.10.50.M" ], "rid_patterns": [ @@ -102,9 +99,11 @@ "abs" ], "aliases": [ - "CPI/3.10001.10.50.M", "abs.cpi.all_groups_annual_rate.australia.may_2026" ], + "source_concepts": [ + "CPI/3.10001.10.50.M" + ], "rid_patterns": [ "abs.cpi.all_groups_annual_rate.australia.{P}.first_print" ], @@ -135,9 +134,11 @@ "abs" ], "aliases": [ - "CPI/3.10001.10.50.M", "abs.cpi_indicator.allgroups.yoy.2026-05" ], + "source_concepts": [ + "CPI/3.10001.10.50.M" + ], "rid_patterns": [ "abs.cpi_indicator.allgroups.yoy.{P}" ], @@ -168,10 +169,12 @@ "abs" ], "aliases": [ - "LF/M3.3.1599.20.AUS.M", "abs.labour.employment_change.australia.june_2026", "abs.labour.employment_change.australia.may_2026" ], + "source_concepts": [ + "LF/M3.3.1599.20.AUS.M" + ], "rid_patterns": [ "abs.labour.employment_change.australia.{P}.first_print" ], @@ -192,6 +195,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -220,10 +224,12 @@ "abs" ], "aliases": [ - "LF/M13.3.1599.20.AUS.M", "abs.labour.unemployment_rate.australia.june_2026", "abs.labour.unemployment_rate.australia.may_2026" ], + "source_concepts": [ + "LF/M13.3.1599.20.AUS.M" + ], "rid_patterns": [ "abs.labour.unemployment_rate.australia.{P}.first_print" ], @@ -256,6 +262,9 @@ "aliases": [ "bank_of_canada.overnight_rate.after_june_2026" ], + "source_concepts": [ + "bank_of_canada.overnight_rate.after_june_2026" + ], "rid_patterns": [ "bank_of_canada.overnight_rate.{P}" ], @@ -276,6 +285,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -304,9 +314,11 @@ "bea" ], "aliases": [ - "DSPI", "bea.disposable_personal_income.level.may_2026" ], + "source_concepts": [ + "DSPI" + ], "rid_patterns": [ "bea.disposable_personal_income.level.{P}.first_print" ], @@ -337,9 +349,11 @@ "bea" ], "aliases": [ - "A063RC1", "bea.government_social_benefits.level.may_2026" ], + "source_concepts": [ + "A063RC1" + ], "rid_patterns": [ "bea.government_social_benefits.level.{P}.first_print" ], @@ -370,9 +384,11 @@ "bea" ], "aliases": [ - "W729RC1", "bea.government_social_benefits.medicaid.may_2026" ], + "source_concepts": [ + "W729RC1" + ], "rid_patterns": [ "bea.government_social_benefits.medicaid.{P}.first_print" ], @@ -403,9 +419,11 @@ "bea" ], "aliases": [ - "W824RC1", "bea.government_social_benefits.medicare.may_2026" ], + "source_concepts": [ + "W824RC1" + ], "rid_patterns": [ "bea.government_social_benefits.medicare.{P}.first_print" ], @@ -436,9 +454,11 @@ "bea" ], "aliases": [ - "W823RC1", "bea.government_social_benefits.social_security.may_2026" ], + "source_concepts": [ + "W823RC1" + ], "rid_patterns": [ "bea.government_social_benefits.social_security.{P}.first_print" ], @@ -469,9 +489,11 @@ "bea" ], "aliases": [ - "PCEPILFE", "bea.pce.core_mom.may_2026" ], + "source_concepts": [ + "PCEPILFE" + ], "rid_patterns": [ "bea.pce.core_mom.{P}.first_print" ], @@ -502,9 +524,11 @@ "bea" ], "aliases": [ - "PCEPI", "bea.pce_price_index.monthly_change.may_2026" ], + "source_concepts": [ + "PCEPI" + ], "rid_patterns": [ "bea.pce_price_index.monthly_change.{P}.first_print" ], @@ -535,9 +559,11 @@ "bea" ], "aliases": [ - "W055RC1", "bea.personal_current_taxes.level.may_2026" ], + "source_concepts": [ + "W055RC1" + ], "rid_patterns": [ "bea.personal_current_taxes.level.{P}.first_print" ], @@ -568,9 +594,11 @@ "bea" ], "aliases": [ - "A191RL1Q225SBEA", "bea.real_gdp.saar.q1_2026.third_estimate" ], + "source_concepts": [ + "A191RL1Q225SBEA" + ], "rid_patterns": [ "bea.real_gdp.saar.{P}.third_estimate" ], @@ -591,6 +619,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -619,9 +648,11 @@ "bea" ], "aliases": [ - "A576RC1", "bea.wages_and_salaries.level.may_2026" ], + "source_concepts": [ + "A576RC1" + ], "rid_patterns": [ "bea.wages_and_salaries.level.{P}.first_print" ], @@ -654,6 +685,9 @@ "aliases": [ "bls.ces.average_hourly_earnings_private" ], + "source_concepts": [ + "bls.ces.average_hourly_earnings_private" + ], "rid_patterns": [ "bls.ces.average_hourly_earnings_private.{P}.first_print" ], @@ -674,6 +708,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -702,9 +737,11 @@ "bls_ces" ], "aliases": [ - "PAYEMS", "bls.ces.total_nonfarm_payroll_change.june_2026" ], + "source_concepts": [ + "PAYEMS" + ], "rid_patterns": [ "bls.ces.total_nonfarm_payroll_change.{P}.first_print" ], @@ -735,6 +772,9 @@ "bls" ], "aliases": [], + "source_concepts": [ + "bls.ces.total_nonfarm_payroll_change" + ], "rid_patterns": [ "bls.ces.total_nonfarm_payroll_change.{P}.first_print" ], @@ -755,6 +795,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -773,6 +814,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -791,6 +833,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -809,6 +852,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -827,6 +871,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -855,9 +900,11 @@ "bls_cpi" ], "aliases": [ - "CPILFESL", "bls.cpi.u.core_mom.june_2026" ], + "source_concepts": [ + "CPILFESL" + ], "rid_patterns": [ "bls.cpi.u.core_mom.{P}.first_print" ], @@ -890,6 +937,9 @@ "aliases": [ "bls.cpi.u.core_mom.may_2026" ], + "source_concepts": [ + "bls.cpi.u.core_mom.may_2026" + ], "rid_patterns": [ "bls.cpi.u.core_mom.{P}.first_print" ], @@ -920,9 +970,11 @@ "bls_cpi" ], "aliases": [ - "CPIAUCSL", "bls.cpi.u.headline_mom.june_2026" ], + "source_concepts": [ + "CPIAUCSL" + ], "rid_patterns": [ "bls.cpi.u.headline_mom.{P}.first_print" ], @@ -955,6 +1007,9 @@ "aliases": [ "bls.cpi.u.headline_mom.may_2026" ], + "source_concepts": [ + "bls.cpi.u.headline_mom.may_2026" + ], "rid_patterns": [ "bls.cpi.u.headline_mom.{P}.first_print" ], @@ -985,9 +1040,11 @@ "bls_cps" ], "aliases": [ - "Business and financial operations occupations", "bls.cps.employed_people_by_occupation.business_financial_operations.june_2026" ], + "source_concepts": [ + "Business and financial operations occupations" + ], "rid_patterns": [ "bls.cps.employed_people_by_occupation.business_financial_operations.{P}.first_print" ], @@ -1018,9 +1075,11 @@ "bls_cps" ], "aliases": [ - "Computer and mathematical occupations", "bls.cps.employed_people_by_occupation.computer_mathematical.june_2026" ], + "source_concepts": [ + "Computer and mathematical occupations" + ], "rid_patterns": [ "bls.cps.employed_people_by_occupation.computer_mathematical.{P}.first_print" ], @@ -1051,9 +1110,11 @@ "bls_cps" ], "aliases": [ - "Healthcare support occupations", "bls.cps.employed_people_by_occupation.healthcare_support.june_2026" ], + "source_concepts": [ + "Healthcare support occupations" + ], "rid_patterns": [ "bls.cps.employed_people_by_occupation.healthcare_support.{P}.first_print" ], @@ -1084,9 +1145,11 @@ "bls_cps" ], "aliases": [ - "Office and administrative support occupations", "bls.cps.employed_people_by_occupation.office_administrative_support.june_2026" ], + "source_concepts": [ + "Office and administrative support occupations" + ], "rid_patterns": [ "bls.cps.employed_people_by_occupation.office_administrative_support.{P}.first_print" ], @@ -1117,9 +1180,11 @@ "bls_cps" ], "aliases": [ - "Production occupations", "bls.cps.employed_people_by_occupation.production.june_2026" ], + "source_concepts": [ + "Production occupations" + ], "rid_patterns": [ "bls.cps.employed_people_by_occupation.production.{P}.first_print" ], @@ -1150,9 +1215,11 @@ "bls_cps" ], "aliases": [ - "Transportation and material moving occupations", "bls.cps.employed_people_by_occupation.transportation_material_moving.june_2026" ], + "source_concepts": [ + "Transportation and material moving occupations" + ], "rid_patterns": [ "bls.cps.employed_people_by_occupation.transportation_material_moving.{P}.first_print" ], @@ -1173,6 +1240,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -1191,6 +1259,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -1219,9 +1288,11 @@ "bls_cps" ], "aliases": [ - "UNRATE", "bls.cps.unemployment_rate.june_2026" ], + "source_concepts": [ + "UNRATE" + ], "rid_patterns": [ "bls.cps.unemployment_rate.{P}.first_print" ], @@ -1252,6 +1323,9 @@ "bls" ], "aliases": [], + "source_concepts": [ + "bls.cps.unemployment_rate" + ], "rid_patterns": [ "bls.cps.unemployment_rate.{P}.first_print" ], @@ -1282,9 +1356,11 @@ "bls_eci" ], "aliases": [ - "ECIWAG", "bls.eci.private_wages_salaries_qoq.2026_q2" ], + "source_concepts": [ + "ECIWAG" + ], "rid_patterns": [ "bls.eci.private_wages_salaries_qoq.{P}.first_print" ], @@ -1315,9 +1391,11 @@ "bls_eci" ], "aliases": [ - "ECICOM", "bls.eci.total_compensation_private_industry_qoq.2026_q2" ], + "source_concepts": [ + "ECICOM" + ], "rid_patterns": [ "bls.eci.total_compensation_private_industry_qoq.{P}.first_print" ], @@ -1338,6 +1416,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -1366,9 +1445,11 @@ "bls_import_export_prices" ], "aliases": [ - "IR", "bls.import_price_index.all_imports_mom.2026-06" ], + "source_concepts": [ + "IR" + ], "rid_patterns": [ "bls.import_price_index.all_imports_mom.{P}.first_print" ], @@ -1399,9 +1480,11 @@ "bls" ], "aliases": [ - "bls.import_price_index.all_imports", "bls.import_price_index.all_imports_mom.may_2026" ], + "source_concepts": [ + "bls.import_price_index.all_imports" + ], "rid_patterns": [ "bls.import_price_index.all_imports_mom.{P}.first_print" ], @@ -1422,6 +1505,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -1450,9 +1534,11 @@ "bls_jolts" ], "aliases": [ - "JTSJOL", "bls.jolts.job_openings.may_2026" ], + "source_concepts": [ + "JTSJOL" + ], "rid_patterns": [ "bls.jolts.job_openings.{P}.first_print" ], @@ -1483,9 +1569,11 @@ "bls_jolts" ], "aliases": [ - "JTSJOL", "bls.jolts.job_openings_total.may_2026" ], + "source_concepts": [ + "JTSJOL" + ], "rid_patterns": [ "bls.jolts.job_openings_total.{P}.first_print" ], @@ -1506,6 +1594,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -1524,6 +1613,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -1554,6 +1644,9 @@ "aliases": [ "bls.ppi.final_demand_monthly_change.may_2026" ], + "source_concepts": [ + "bls.ppi.final_demand_monthly_change.may_2026" + ], "rid_patterns": [ "bls.ppi.final_demand_monthly_change.{P}.first_print" ], @@ -1574,6 +1667,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -1592,6 +1686,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -1610,6 +1705,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -1641,6 +1737,9 @@ "boe.bank_rate.2026_06_18", "boe.bank_rate.after_mpc_june_2026" ], + "source_concepts": [ + "boe.bank_rate" + ], "rid_patterns": [ "boe.bank_rate.{P}", "boe.bank_rate.{P}.first_print" @@ -1672,9 +1771,11 @@ "boj" ], "aliases": [ - "boj.guideline_uncollateralized_overnight_call_rate", "boj.policy_rate_guideline.after_june_2026" ], + "source_concepts": [ + "boj.guideline_uncollateralized_overnight_call_rate" + ], "rid_patterns": [ "boj.policy_rate_guideline.{P}" ], @@ -1695,6 +1796,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -1713,6 +1815,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -1731,6 +1834,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -1761,6 +1865,9 @@ "aliases": [ "census.housing_starts.saar.may_2026" ], + "source_concepts": [ + "census.housing_starts.saar" + ], "rid_patterns": [ "census.housing_starts.saar.{P}.first_print" ], @@ -1791,9 +1898,11 @@ "census_housing" ], "aliases": [ - "HOUST", "census.housing_starts.saar.2026-06" ], + "source_concepts": [ + "HOUST" + ], "rid_patterns": [ "census.housing_starts.saar.{P}.first_print" ], @@ -1824,9 +1933,11 @@ "census_m3" ], "aliases": [ - "DGORDER", "census.m3.durable_goods_new_orders_mom.2026_06" ], + "source_concepts": [ + "DGORDER" + ], "rid_patterns": [ "census.m3.durable_goods_new_orders_mom.{P}.first_print" ], @@ -1857,9 +1968,11 @@ "census_m3" ], "aliases": [ - "AMDMVS", "census.m3.durable_goods_shipments_mom.2026_06" ], + "source_concepts": [ + "AMDMVS" + ], "rid_patterns": [ "census.m3.durable_goods_shipments_mom.{P}.first_print" ], @@ -1890,7 +2003,9 @@ "census" ], "aliases": [ - "census.marts.adv44x72.may_2026.monthly_change", + "census.marts.adv44x72.may_2026.monthly_change" + ], + "source_concepts": [ "census.marts.advance_retail_and_food_services_sales_mom" ], "rid_patterns": [ @@ -1923,9 +2038,11 @@ "census" ], "aliases": [ - "census.mtis.total_business_inventories", "census.mtis.total_business_inventories_level.april_2026" ], + "source_concepts": [ + "census.mtis.total_business_inventories" + ], "rid_patterns": [ "census.mtis.total_business_inventories_level.{P}.first_print" ], @@ -1946,6 +2063,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -1974,9 +2092,11 @@ "cms_provider_data" ], "aliases": [ - "Average Number of Residents per Day / Number of Certified Beds", "cms.care_compare.nursing_home_occupancy_pct.2026-07" ], + "source_concepts": [ + "Average Number of Residents per Day / Number of Certified Beds" + ], "rid_patterns": [ "cms.care_compare.nursing_home_occupancy_pct.{P}.first_print" ], @@ -2006,7 +2126,8 @@ "sources": [ "cms" ], - "aliases": [ + "aliases": [], + "source_concepts": [ "Beneficiaries Disenrolled for Procedural Reasons at Renewal" ], "rid_patterns": [ @@ -2038,7 +2159,8 @@ "sources": [ "cms" ], - "aliases": [ + "aliases": [], + "source_concepts": [ "Beneficiaries Disenrolled at Renewal (Total)" ], "rid_patterns": [ @@ -2070,7 +2192,8 @@ "sources": [ "cms" ], - "aliases": [ + "aliases": [], + "source_concepts": [ "Beneficiaries Whose Coverage Was Renewed on an Ex Parte Basis" ], "rid_patterns": [ @@ -2102,7 +2225,8 @@ "sources": [ "cms" ], - "aliases": [ + "aliases": [], + "source_concepts": [ "Beneficiaries Whose Coverage Was Renewed (Total)" ], "rid_patterns": [ @@ -2135,9 +2259,11 @@ "cms_provider_data" ], "aliases": [ - "Reported Total Nurse Staffing Hours per Resident per Day", "cms.nursing_home_compare.reported_total_nurse_staffing_hprd_us.2026-07" ], + "source_concepts": [ + "Reported Total Nurse Staffing Hours per Resident per Day" + ], "rid_patterns": [ "cms.nursing_home_compare.reported_total_nurse_staffing_hprd_us.{P}.first_print" ], @@ -2167,7 +2293,8 @@ "sources": [ "dol_eta" ], - "aliases": [ + "aliases": [], + "source_concepts": [ "CCSA" ], "rid_patterns": [ @@ -2202,6 +2329,9 @@ "aliases": [ "dol.eta.initial_claims.sa.week_ending_2026_06_06" ], + "source_concepts": [ + "dol.eta.initial_claims.sa.week_ending_2026_06_06" + ], "rid_patterns": [ "dol.eta.initial_claims.sa.{P}" ], @@ -2234,6 +2364,9 @@ "aliases": [ "ecb.deposit_facility_rate.after_june_2026" ], + "source_concepts": [ + "ecb.deposit_facility_rate.after_june_2026" + ], "rid_patterns": [ "ecb.deposit_facility_rate.{P}" ], @@ -2264,7 +2397,9 @@ "statjp" ], "aliases": [ - "estat.jp.cpi.core_exfreshfood.yoy.2026_05", + "estat.jp.cpi.core_exfreshfood.yoy.2026_05" + ], + "source_concepts": [ "japan.cpi.all_items_less_fresh_food_yoy" ], "rid_patterns": [ @@ -2298,7 +2433,9 @@ "eurostat" ], "aliases": [ - "eurostat.ea.hicp.flash.yoy.2026-06", + "eurostat.ea.hicp.flash.yoy.2026-06" + ], + "source_concepts": [ "prc_hicp_minr/M.RCH_A.TOTAL.EA21" ], "rid_patterns": [ @@ -2309,7 +2446,7 @@ "observation_count": 2 }, { - "uuid": "815299ce-841a-4115-9af2-3b52fc036834", + "uuid": "74ce2da4-28ed-409a-a0f1-2a7c40dc7caf", "concept": "eurostat.hicp.all_items_annual_rate.euro_area", "family_patterns": [ "eurostat.hicp.all_items_annual_rate.euro_area.{P}" @@ -2319,29 +2456,32 @@ "cadence": "month", "geography": { "level": "region", - "id": "EA", + "id": "EA21", "vintage": "current", "name": "Euro area" }, "entity": { - "name": "household", - "role": "hicp_all_items" + "name": "economy", + "role": "aggregate" }, "sources": [ "eurostat" ], "aliases": [ - "eurostat.hicp.all_items_annual_rate.euro_area.may_2026" + "eurostat.hicp.all_items_annual_rate.euro_area.june_2026" + ], + "source_concepts": [ + "prc_hicp_minr/M.RCH_A.TOTAL.EA21" ], "rid_patterns": [ - "eurostat.hicp.all_items_annual_rate.euro_area.{P}.final_first_print" + "eurostat.hicp.all_items_annual_rate.euro_area.{P}.flash" ], - "first_observed_period": "2026-05", - "last_observed_period": "2026-05", + "first_observed_period": "2026-06", + "last_observed_period": "2026-06", "observation_count": 1 }, { - "uuid": "74ce2da4-28ed-409a-a0f1-2a7c40dc7caf", + "uuid": "815299ce-841a-4115-9af2-3b52fc036834", "concept": "eurostat.hicp.all_items_annual_rate.euro_area", "family_patterns": [ "eurostat.hicp.all_items_annual_rate.euro_area.{P}" @@ -2351,26 +2491,28 @@ "cadence": "month", "geography": { "level": "region", - "id": "EA21", + "id": "EA", "vintage": "current", "name": "Euro area" }, "entity": { - "name": "economy", - "role": "aggregate" + "name": "household", + "role": "hicp_all_items" }, "sources": [ "eurostat" ], "aliases": [ - "eurostat.hicp.all_items_annual_rate.euro_area.june_2026", - "prc_hicp_minr/M.RCH_A.TOTAL.EA21" + "eurostat.hicp.all_items_annual_rate.euro_area.may_2026" + ], + "source_concepts": [ + "eurostat.hicp.all_items_annual_rate.euro_area" ], "rid_patterns": [ - "eurostat.hicp.all_items_annual_rate.euro_area.{P}.flash" + "eurostat.hicp.all_items_annual_rate.euro_area.{P}.final_first_print" ], - "first_observed_period": "2026-06", - "last_observed_period": "2026-06", + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", "observation_count": 1 }, { @@ -2386,6 +2528,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -2416,6 +2559,9 @@ "aliases": [ "eurostat.industrial_production.euro_area.april_2026" ], + "source_concepts": [ + "eurostat.industrial_production.euro_area" + ], "rid_patterns": [ "eurostat.industrial_production.euro_area.{P}.first_print" ], @@ -2446,7 +2592,9 @@ "eurostat" ], "aliases": [ - "eurostat.retail_trade.volume_mom.euro_area.may_2026", + "eurostat.retail_trade.volume_mom.euro_area.may_2026" + ], + "source_concepts": [ "products-euro-indicators release page (euro area headline)" ], "rid_patterns": [ @@ -2469,6 +2617,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -2491,6 +2640,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -2519,7 +2669,9 @@ "eurostat" ], "aliases": [ - "eurostat.unemployment_rate.euro_area.may_2026", + "eurostat.unemployment_rate.euro_area.may_2026" + ], + "source_concepts": [ "une_rt_m/M.SA.TOTAL.PC_ACT.T.EA21" ], "rid_patterns": [ @@ -2542,6 +2694,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -2570,9 +2723,11 @@ "federal_reserve_g17" ], "aliases": [ - "TCU", "fed.g17.capacity_utilization.total_industry.2026-06" ], + "source_concepts": [ + "TCU" + ], "rid_patterns": [ "fed.g17.capacity_utilization.total_industry.{P}.first_print" ], @@ -2605,6 +2760,9 @@ "aliases": [ "fed.g17.capacity_utilization.total_industry.may_2026" ], + "source_concepts": [ + "fed.g17.capacity_utilization.total_industry" + ], "rid_patterns": [ "fed.g17.capacity_utilization.total_industry.{P}.first_print" ], @@ -2635,9 +2793,11 @@ "federal_reserve_g17" ], "aliases": [ - "INDPRO", "fed.g17.industrial_production.total_index_mom.2026-06" ], + "source_concepts": [ + "INDPRO" + ], "rid_patterns": [ "fed.g17.industrial_production.total_index_mom.{P}.first_print" ], @@ -2670,6 +2830,9 @@ "aliases": [ "fed.g17.industrial_production.total_index_mom.may_2026" ], + "source_concepts": [ + "fed.g17.industrial_production.total_index_mom" + ], "rid_patterns": [ "fed.g17.industrial_production.total_index_mom.{P}.first_print" ], @@ -2690,6 +2853,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -2708,6 +2872,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -2726,6 +2891,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -2744,6 +2910,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -2772,6 +2939,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.application_processing_timeliness_rate" + ], "rid_patterns": [ "fns.snap.application_processing_timeliness.california.{P}.official_release" ], @@ -2802,6 +2972,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.overpayment_error_rate" + ], "rid_patterns": [ "fns.snap.overpayment_payment_error_rate.us.{P}.official_release" ], @@ -2831,7 +3004,8 @@ "sources": [ "fns" ], - "aliases": [ + "aliases": [], + "source_concepts": [ "fns.snap.total_payment_error_rate" ], "rid_patterns": [ @@ -2864,6 +3038,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.us.{P}", "fns.snap.total_payment_error_rate.us.{P}.official_release" @@ -2895,6 +3072,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.al.{P}" ], @@ -2925,6 +3105,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.ak.{P}" ], @@ -2955,6 +3138,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.az.{P}" ], @@ -2985,6 +3171,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.ar.{P}" ], @@ -3015,6 +3204,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.ca.{P}" ], @@ -3045,6 +3237,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.co.{P}" ], @@ -3075,6 +3270,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.ct.{P}" ], @@ -3105,6 +3303,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.de.{P}" ], @@ -3135,6 +3336,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.dc.{P}" ], @@ -3165,6 +3369,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.fl.{P}" ], @@ -3195,6 +3402,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.ga.{P}" ], @@ -3225,6 +3435,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.hi.{P}" ], @@ -3255,6 +3468,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.id.{P}" ], @@ -3285,6 +3501,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.il.{P}" ], @@ -3315,6 +3534,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.in.{P}" ], @@ -3345,6 +3567,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.ia.{P}" ], @@ -3375,6 +3600,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.ks.{P}" ], @@ -3405,6 +3633,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.ky.{P}" ], @@ -3435,6 +3666,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.la.{P}" ], @@ -3465,6 +3699,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.me.{P}" ], @@ -3495,6 +3732,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.md.{P}" ], @@ -3525,6 +3765,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.ma.{P}" ], @@ -3555,6 +3798,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.mi.{P}" ], @@ -3585,6 +3831,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.mn.{P}" ], @@ -3615,6 +3864,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.ms.{P}" ], @@ -3645,6 +3897,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.mo.{P}" ], @@ -3675,6 +3930,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.mt.{P}" ], @@ -3705,6 +3963,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.ne.{P}" ], @@ -3735,6 +3996,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.nv.{P}" ], @@ -3765,6 +4029,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.nh.{P}" ], @@ -3795,6 +4062,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.nj.{P}" ], @@ -3825,6 +4095,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.nm.{P}" ], @@ -3855,6 +4128,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.ny.{P}" ], @@ -3885,6 +4161,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.nc.{P}" ], @@ -3915,6 +4194,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.nd.{P}" ], @@ -3945,6 +4227,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.oh.{P}" ], @@ -3975,6 +4260,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.ok.{P}" ], @@ -4005,6 +4293,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.or.{P}" ], @@ -4035,6 +4326,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.pa.{P}" ], @@ -4065,6 +4359,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.ri.{P}" ], @@ -4095,6 +4392,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.sc.{P}" ], @@ -4125,6 +4425,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.sd.{P}" ], @@ -4155,6 +4458,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.tn.{P}" ], @@ -4185,6 +4491,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.tx.{P}" ], @@ -4215,6 +4524,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.ut.{P}" ], @@ -4245,6 +4557,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.vt.{P}" ], @@ -4275,6 +4590,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.va.{P}" ], @@ -4305,6 +4623,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.wa.{P}" ], @@ -4335,6 +4656,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.wv.{P}" ], @@ -4365,6 +4689,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.wi.{P}" ], @@ -4395,6 +4722,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.wy.{P}" ], @@ -4425,6 +4755,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.gu.{P}" ], @@ -4455,6 +4788,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.total_payment_error_rate" + ], "rid_patterns": [ "fns.snap.total_payment_error_rate.vi.{P}" ], @@ -4475,6 +4811,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -4503,6 +4840,9 @@ "fns" ], "aliases": [], + "source_concepts": [ + "fns.snap.underpayment_error_rate" + ], "rid_patterns": [ "fns.snap.underpayment_payment_error_rate.us.{P}.official_release" ], @@ -4523,6 +4863,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -4545,6 +4886,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -4567,6 +4909,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -4589,6 +4932,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -4619,6 +4963,9 @@ "aliases": [ "ons.cpi.annual_rate.may_2026" ], + "source_concepts": [ + "ons.cpi.annual_rate" + ], "rid_patterns": [ "ons.cpi.annual_rate.{P}.first_print" ], @@ -4651,6 +4998,9 @@ "aliases": [ "ons.cpih.annual_rate.2026_05" ], + "source_concepts": [ + "ons.cpih.annual_rate" + ], "rid_patterns": [ "ons.cpih.annual_rate.{P}" ], @@ -4683,6 +5033,9 @@ "aliases": [ "ons.gdp.monthly_growth.april_2026" ], + "source_concepts": [ + "ons.gdp.monthly_growth.april_2026" + ], "rid_patterns": [ "ons.gdp.monthly_growth.{P}.first_print" ], @@ -4715,6 +5068,9 @@ "aliases": [ "ons.hmrc.paye_payrolled_employees.may_2026" ], + "source_concepts": [ + "ons.hmrc.paye_payrolled_employees" + ], "rid_patterns": [ "ons.hmrc.paye_payrolled_employees.{P}.first_print" ], @@ -4747,6 +5103,9 @@ "aliases": [ "ons.labour.unemployment_rate.february_to_april_2026" ], + "source_concepts": [ + "ons.labour.unemployment_rate" + ], "rid_patterns": [ "ons.labour.unemployment_rate.{P}.first_print" ], @@ -4777,9 +5136,11 @@ "ons" ], "aliases": [ - "ons.pusf.j5ii", "ons.pusf.j5ii.public_sector_net_borrowing_ex_banks.may_2026" ], + "source_concepts": [ + "ons.pusf.j5ii" + ], "rid_patterns": [ "ons.pusf.j5ii.public_sector_net_borrowing_ex_banks.{P}.first_print" ], @@ -4812,6 +5173,9 @@ "aliases": [ "ons.retail_sales.volume_mom.may_2026" ], + "source_concepts": [ + "ons.retail_sales.volume_mom" + ], "rid_patterns": [ "ons.retail_sales.volume_mom.{P}.first_print" ], @@ -4844,6 +5208,9 @@ "aliases": [ "rba.cash_rate_target.after_june_2026" ], + "source_concepts": [ + "rba.cash_rate_target" + ], "rid_patterns": [ "rba.cash_rate_target.{P}" ], @@ -4864,6 +5231,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -4886,6 +5254,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -4908,6 +5277,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -4935,7 +5305,8 @@ "sources": [ "statcan" ], - "aliases": [ + "aliases": [], + "source_concepts": [ "v65201210" ], "rid_patterns": [ @@ -4970,6 +5341,9 @@ "aliases": [ "statcan.building_permits.total_value_mom.canada.april_2026" ], + "source_concepts": [ + "statcan.building_permits.total_value_mom.canada.april_2026" + ], "rid_patterns": [ "statcan.building_permits.total_value_mom.canada.{P}.first_print" ], @@ -5000,7 +5374,9 @@ "statcan" ], "aliases": [ - "statcan.cpi.all_items_annual_rate.canada.may_2026", + "statcan.cpi.all_items_annual_rate.canada.may_2026" + ], + "source_concepts": [ "v41690973" ], "rid_patterns": [ @@ -5033,7 +5409,9 @@ "statcan" ], "aliases": [ - "statcan.cpi.allitems.yoy.2026-05", + "statcan.cpi.allitems.yoy.2026-05" + ], + "source_concepts": [ "v41690973" ], "rid_patterns": [ @@ -5066,7 +5444,9 @@ "statcan" ], "aliases": [ - "statcan.employment_insurance.regular_beneficiaries.canada.may_2026", + "statcan.employment_insurance.regular_beneficiaries.canada.may_2026" + ], + "source_concepts": [ "v64549350" ], "rid_patterns": [ @@ -5102,6 +5482,9 @@ "statcan.employment_insurance.regular_beneficiaries", "statcan.employment_insurance.regular_beneficiaries.canada.april_2026" ], + "source_concepts": [ + "statcan.employment_insurance.regular_beneficiaries" + ], "rid_patterns": [ "statcan.employment_insurance.regular_beneficiaries.canada.{P}.first_print" ], @@ -5132,7 +5515,9 @@ "statcan" ], "aliases": [ - "statcan.gdp_by_industry.monthly_growth.april_2026", + "statcan.gdp_by_industry.monthly_growth.april_2026" + ], + "source_concepts": [ "v65201210" ], "rid_patterns": [ @@ -5165,6 +5550,9 @@ "statcan" ], "aliases": [], + "source_concepts": [ + "statcan.lfs.employment_change" + ], "rid_patterns": [ "statcan.lfs.employment_change.canada.{P}.first_print" ], @@ -5195,6 +5583,9 @@ "statcan" ], "aliases": [], + "source_concepts": [ + "statcan.lfs.unemployment_rate" + ], "rid_patterns": [ "statcan.lfs.unemployment_rate.canada.{P}.first_print" ], @@ -5225,9 +5616,11 @@ "statcan" ], "aliases": [ - "statcan.retail_trade.sales_mom", "statcan.retail_trade.sales_mom.canada.april_2026" ], + "source_concepts": [ + "statcan.retail_trade.sales_mom" + ], "rid_patterns": [ "statcan.retail_trade.sales_mom.canada.{P}.first_print" ], @@ -5258,9 +5651,11 @@ "statcan" ], "aliases": [ - "statcan.wholesale_trade.sales_mom_exclusions", "statcan.wholesale_trade.sales_mom_exclusions.canada.april_2026" ], + "source_concepts": [ + "statcan.wholesale_trade.sales_mom_exclusions" + ], "rid_patterns": [ "statcan.wholesale_trade.sales_mom_exclusions.canada.{P}.first_print" ], @@ -5291,9 +5686,11 @@ "statjp" ], "aliases": [ - "japan.cpi.all_items_yoy", "statjp.cpi.all_items_annual_rate.japan.may_2026" ], + "source_concepts": [ + "japan.cpi.all_items_yoy" + ], "rid_patterns": [ "statjp.cpi.all_items_annual_rate.japan.{P}.first_print" ], @@ -5324,9 +5721,11 @@ "stat_jp" ], "aliases": [ - "e-Stat statInfId 000040461676 (series 0001, all items)", "statjp.cpi.tokyo_all_items_annual_rate.june_2026" ], + "source_concepts": [ + "e-Stat statInfId 000040461676 (series 0001, all items)" + ], "rid_patterns": [ "statjp.cpi.tokyo_all_items_annual_rate.{P}.preliminary" ], @@ -5347,6 +5746,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -5375,9 +5775,11 @@ "stat_jp" ], "aliases": [ - "stat.go.jp kakei sokuhou monthly page", "statjp.household_spending.real_yoy.two_or_more_person_households.may_2026" ], + "source_concepts": [ + "stat.go.jp kakei sokuhou monthly page" + ], "rid_patterns": [ "statjp.household_spending.real_yoy.two_or_more_person_households.{P}.first_print" ], @@ -5408,9 +5810,11 @@ "stat_jp" ], "aliases": [ - "stat.go.jp roudou sokuhou monthly page", "statjp.lfs.unemployment_rate.japan.may_2026" ], + "source_concepts": [ + "stat.go.jp roudou sokuhou monthly page" + ], "rid_patterns": [ "statjp.lfs.unemployment_rate.japan.{P}.first_print" ], @@ -5443,6 +5847,9 @@ "aliases": [ "treasury.mts.monthly_deficit.may_2026" ], + "source_concepts": [ + "treasury.mts.monthly_deficit.may_2026" + ], "rid_patterns": [ "treasury.mts.monthly_deficit.{P}.first_print" ], @@ -5473,10 +5880,12 @@ "bea" ], "aliases": [ - "PCEPILFE", "us.bea.core_pce.mom_sa.2026-05", "us.bea.core_pce.mom_sa.2026-06" ], + "source_concepts": [ + "PCEPILFE" + ], "rid_patterns": [ "us.bea.core_pce.mom_sa.{P}" ], @@ -5507,9 +5916,11 @@ "census" ], "aliases": [ - "census.housing_starts.saar", "us.census.housing_starts.total_saar.2026_05" ], + "source_concepts": [ + "census.housing_starts.saar" + ], "rid_patterns": [ "us.census.housing_starts.total_saar.{P}" ], @@ -5539,7 +5950,8 @@ "sources": [ "dol_eta" ], - "aliases": [ + "aliases": [], + "source_concepts": [ "ICSA" ], "rid_patterns": [ @@ -5572,9 +5984,11 @@ "dol" ], "aliases": [ - "dol.eta.initial_claims.sa", "us.dol.initial_claims.sa.week_2026_06_13" ], + "source_concepts": [ + "dol.eta.initial_claims.sa" + ], "rid_patterns": [ "us.dol.initial_claims.sa.{P}" ], @@ -5605,9 +6019,11 @@ "fed" ], "aliases": [ - "fomc.federal_funds_target_range_upper", "us.fed.fomc.target_range_upper.2026_06" ], + "source_concepts": [ + "fomc.federal_funds_target_range_upper" + ], "rid_patterns": [ "us.fed.fomc.target_range_upper.{P}" ], @@ -5638,9 +6054,11 @@ "fed" ], "aliases": [ - "fed.g17.industrial_production.total_index_mom", "us.frb.industrial_production.total.mom_sa.2026_05" ], + "source_concepts": [ + "fed.g17.industrial_production.total_index_mom" + ], "rid_patterns": [ "us.frb.industrial_production.total.mom_sa.{P}" ], @@ -5661,6 +6079,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -5679,6 +6098,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -5697,6 +6117,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -5715,6 +6136,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -5733,6 +6155,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -5751,6 +6174,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, @@ -5773,6 +6197,7 @@ "entity": null, "sources": [], "aliases": [], + "source_concepts": [], "rid_patterns": [], "first_observed_period": null, "last_observed_period": null, diff --git a/ledger/series_uuid_registry.jsonl b/ledger/series_uuid_registry.jsonl new file mode 100644 index 0000000..cba4249 --- /dev/null +++ b/ledger/series_uuid_registry.jsonl @@ -0,0 +1,201 @@ +{"concept": "abs.building_approvals.total_dwellings_mom.australia", "geography": {"level": "country", "id": "AU", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "efbb2901-f8f8-4f5b-9835-56e5cc3e62ff"} +{"concept": "abs.cpi.all_groups.yoy", "geography": {"level": "country", "id": "AU", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "92220fc5-fd61-46f5-b02d-467948889e2d"} +{"concept": "abs.cpi.all_groups_annual_rate.australia", "geography": {"level": "country", "id": "AU", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "88055a40-bfac-468f-bec6-fcc768e7fc7b"} +{"concept": "abs.cpi_indicator.allgroups.yoy", "geography": {"level": "country", "id": "AU", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "b4d4b454-c8a7-42a4-9a28-ce393b664717"} +{"concept": "abs.labour.employment_change.australia", "geography": {"level": "country", "id": "AU", "vintage": "current"}, "entity": {"name": "person", "role": "employed"}, "uuid": "ab7e3632-e9d3-453e-bdc5-aa5a8dca77c9"} +{"concept": "abs.labour.unemployment_rate", "geography": null, "entity": null, "uuid": "443cb1bf-49d3-4e1e-9897-9556c8a8c768"} +{"concept": "abs.labour.unemployment_rate.australia", "geography": {"level": "country", "id": "AU", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "980c42b6-db11-4b46-a2b5-e7cd6c67527a"} +{"concept": "bank_of_canada.overnight_rate", "geography": {"level": "country", "id": "CA", "vintage": "current"}, "entity": {"name": "government", "role": "overnight_target"}, "uuid": "1d3d8ca7-091e-4098-9abc-38bd906e7c7a"} +{"concept": "bea.core_pce.mom", "geography": null, "entity": null, "uuid": "efc3259b-eb44-49c3-9bc6-e92fd7cf6243"} +{"concept": "bea.disposable_personal_income.level", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "ff8df856-08d4-4d5b-8228-e51cff7e1a03"} +{"concept": "bea.government_social_benefits.level", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "5271091d-451c-47dc-a3af-5b1581af13ba"} +{"concept": "bea.government_social_benefits.medicaid", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "ed4dcaa2-e7aa-4696-98c2-b82fa81ee5d0"} +{"concept": "bea.government_social_benefits.medicare", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "c3bd2028-88b3-48e4-b648-aca7be6c0434"} +{"concept": "bea.government_social_benefits.social_security", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "e63e1fb2-2db5-4068-80da-8caf1a3faa93"} +{"concept": "bea.pce.core_mom", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "174240ec-f8ff-417a-bf1c-bdf088268a7c"} +{"concept": "bea.pce_price_index.monthly_change", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "0bcbb3b9-801b-435e-8aa8-646c0ac76445"} +{"concept": "bea.personal_current_taxes.level", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "c05ca17a-461c-4ae4-b685-5e6ba8ff7710"} +{"concept": "bea.real_gdp.saar.third_estimate", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "83094ec4-c890-435b-8a6a-83273c3e9077"} +{"concept": "bea.trade.goods_services_deficit", "geography": null, "entity": null, "uuid": "7be75093-832c-4734-a002-a0ff19c610ce"} +{"concept": "bea.wages_and_salaries.level", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "27202e84-7743-46ed-a6e8-2c907aa79a6c"} +{"concept": "bls.ces.average_hourly_earnings_private_monthly_change", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "person", "role": "private_nonfarm_payroll_employee"}, "uuid": "0edb7ad9-0541-4bd2-9597-bffc9e73cd92"} +{"concept": "bls.ces.nonfarm_payrolls.change", "geography": null, "entity": null, "uuid": "d8ab9f3a-aa0c-4b7f-89df-a0c9107cfdbb"} +{"concept": "bls.ces.total_nonfarm_payroll_change", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "c0515530-c56b-417d-9ead-aff423c87207"} +{"concept": "bls.ces.total_nonfarm_payroll_change", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "person", "role": "nonfarm_payroll_employee"}, "uuid": "0b4f36be-2bc7-430b-b960-5df4011347bb"} +{"concept": "bls.cpi.owners_equivalent_rent_mom", "geography": null, "entity": null, "uuid": "d0dc87fb-fd6d-4234-832a-869d5b2d8f3d"} +{"concept": "bls.cpi.rent_primary_residence_mom", "geography": null, "entity": null, "uuid": "615ac1b1-27e8-437d-bf69-86d760488dc0"} +{"concept": "bls.cpi.services_less_energy_mom", "geography": null, "entity": null, "uuid": "3bab6696-88c0-44c8-988c-737d2d00c37c"} +{"concept": "bls.cpi.services_less_rent_shelter_mom", "geography": null, "entity": null, "uuid": "975e942b-d456-480d-859f-a9ffaffcfe34"} +{"concept": "bls.cpi.shelter_mom", "geography": null, "entity": null, "uuid": "4c5b4292-0bcf-4c42-9fc5-39d22e791f75"} +{"concept": "bls.cpi.u.core_mom", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "e5e402d5-1a65-4c5d-a67a-07e3cf03cbf8"} +{"concept": "bls.cpi.u.core_mom", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "household", "role": "cpi_u_less_food_energy"}, "uuid": "d85c3c7f-874b-4caa-bfad-1ec7a473c293"} +{"concept": "bls.cpi.u.headline_mom", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "70ca2ecf-4323-4453-b42f-23350cb95f22"} +{"concept": "bls.cpi.u.headline_mom", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "household", "role": "cpi_u_all_items"}, "uuid": "3e796803-a194-4c83-9dc9-27cc870ff08e"} +{"concept": "bls.cps.employed_people_by_occupation.business_financial_operations", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "3cbdbc7f-3e6b-4b76-8758-57e519906bd4"} +{"concept": "bls.cps.employed_people_by_occupation.computer_mathematical", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "54f19551-6288-48e2-9fa0-17933e55d390"} +{"concept": "bls.cps.employed_people_by_occupation.healthcare_support", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "25457957-df59-4e16-ae3f-431b4c63e7bf"} +{"concept": "bls.cps.employed_people_by_occupation.office_administrative_support", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "2316f164-f13f-4025-980c-b92cc46b4d1a"} +{"concept": "bls.cps.employed_people_by_occupation.production", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "3240539a-c5b8-43de-a240-cc6754fb59aa"} +{"concept": "bls.cps.employed_people_by_occupation.transportation_material_moving", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "e2502908-1e16-46e3-b330-4d6612e510c2"} +{"concept": "bls.cps.telework_share", "geography": null, "entity": null, "uuid": "94a5a046-ea77-449a-8ba9-95ffebc9039c"} +{"concept": "bls.cps.u6_underemployment_rate", "geography": null, "entity": null, "uuid": "e0be267e-d74c-4ad2-a5cd-5ac9f821e018"} +{"concept": "bls.cps.unemployment_rate", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "db2f3857-e0fa-48f8-9ab1-6c8989307fd2"} +{"concept": "bls.cps.unemployment_rate", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "person", "role": "civilian_labor_force"}, "uuid": "8dbbd54f-4bfd-4735-ad0d-5b55b8bb4ec5"} +{"concept": "bls.eci.private_wages_salaries_qoq", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "6d2e1191-9e14-4719-a2bb-c3561fca49e6"} +{"concept": "bls.eci.total_compensation_private_industry_qoq", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "2820e965-4c7b-40d2-b5b8-e03792cf3fbf"} +{"concept": "bls.export_prices.all_commodities_mom", "geography": null, "entity": null, "uuid": "7dc8b980-8f14-4581-ad7c-3963de6a42d9"} +{"concept": "bls.import_price_index.all_imports_mom", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "1d271205-0e71-4a74-9d5f-47e77577e2b2"} +{"concept": "bls.import_price_index.all_imports_mom", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "household", "role": "all_imports"}, "uuid": "e8e6e43e-2c46-4620-a070-91d2da004b1c"} +{"concept": "bls.jolts.hires_rate", "geography": null, "entity": null, "uuid": "3bddc6d0-e581-42dc-9c13-b731a9ede21f"} +{"concept": "bls.jolts.job_openings", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "020dafc2-2ca3-453b-a6bc-f96d443a5f77"} +{"concept": "bls.jolts.job_openings_total", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "1d05d94d-0288-46f0-b6a9-1fa499e85fff"} +{"concept": "bls.jolts.quits_rate", "geography": null, "entity": null, "uuid": "e6aed5ae-2f23-413b-a600-4c5b80edeb18"} +{"concept": "bls.lns11300000", "geography": null, "entity": null, "uuid": "0006af01-72ba-4e63-8d88-54500588f215"} +{"concept": "bls.ppi.final_demand_monthly_change", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "household", "role": "ppi_final_demand"}, "uuid": "a7ec4618-1ade-4f5e-bbbd-1c0680c7fccd"} +{"concept": "bls.productivity.nonfarm_qoq_prelim", "geography": null, "entity": null, "uuid": "6e671a89-65f6-4da9-b092-4e6d74a36a08"} +{"concept": "bls.productivity.nonfarm_unit_labor_costs_qoq_prelim", "geography": null, "entity": null, "uuid": "205885c4-de46-47df-a916-253ae2a0299a"} +{"concept": "bls.real_earnings.avg_hourly_mom", "geography": null, "entity": null, "uuid": "2e3a7126-41fd-4d88-99b6-cf4f2057c277"} +{"concept": "boe.bank_rate", "geography": {"level": "country", "id": "GB", "vintage": "current"}, "entity": {"name": "government", "role": "bank_rate"}, "uuid": "8fb890ac-38d0-430d-9a35-bbeffa0cf8ca"} +{"concept": "boj.policy_rate_guideline", "geography": {"level": "country", "id": "JP", "vintage": "current"}, "entity": {"name": "government", "role": "uncollateralized_overnight_call_rate_guideline"}, "uuid": "e300b533-d2f0-41b4-8cbb-59168f8c80bd"} +{"concept": "census.construction_spending.total_mom", "geography": null, "entity": null, "uuid": "63222847-045a-4784-bd89-4d7878a2a6d8"} +{"concept": "census.housing.completions_saar", "geography": null, "entity": null, "uuid": "e16080e3-02e9-4ae1-95c6-3ded737cf697"} +{"concept": "census.housing.permits_saar", "geography": null, "entity": null, "uuid": "420c8935-70b8-4d8f-a82d-e772446adc6e"} +{"concept": "census.housing_starts.saar", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "dwelling", "role": "housing_start"}, "uuid": "d0212a1e-b360-460a-80a8-1cf4ad5ce427"} +{"concept": "census.housing_starts.saar", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "8c999902-84e2-4e90-b49c-325cd535062a"} +{"concept": "census.m3.durable_goods_new_orders_mom", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "294d82e8-3711-4d0d-89f6-a5a7a087f1be"} +{"concept": "census.m3.durable_goods_shipments_mom", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "5cbc4c1a-e387-4b64-bb56-67ef349fbd51"} +{"concept": "census.marts.adv44x72.monthly_change", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "institutional_sector", "role": "retail_and_food_services_sales"}, "uuid": "ed3215ed-43ec-44dc-b11c-f72a098fc008"} +{"concept": "census.mtis.total_business_inventories_level", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "institutional_sector", "role": "total_business_inventory"}, "uuid": "26bb1475-8685-4142-8472-7bcd6d998fcc"} +{"concept": "census.new_residential_sales.new_single_family_houses_sold_saar", "geography": null, "entity": null, "uuid": "5654403e-f6c5-412e-94e0-feeda778ad81"} +{"concept": "cms.care_compare.nursing_home_occupancy_pct", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "aafb4ff0-cb04-4f76-96de-ef0baaa7bd93"} +{"concept": "cms.medicaid_pi.beneficiaries_disenrolled_procedural", "geography": {"level": "state", "id": "0400000US06", "vintage": "current"}, "entity": {"name": "person", "role": "medicaid_beneficiary"}, "uuid": "fc7c5e2f-90ff-4a9b-95cb-8d339fc8a89d"} +{"concept": "cms.medicaid_pi.beneficiaries_disenrolled_total", "geography": {"level": "state", "id": "0400000US06", "vintage": "current"}, "entity": {"name": "person", "role": "medicaid_beneficiary"}, "uuid": "8829b01a-9f45-4c9e-b38e-d04f405f238c"} +{"concept": "cms.medicaid_pi.beneficiaries_renewed_ex_parte", "geography": {"level": "state", "id": "0400000US06", "vintage": "current"}, "entity": {"name": "person", "role": "medicaid_beneficiary"}, "uuid": "56baf17e-830f-40ea-82f3-aa05cbbeeea2"} +{"concept": "cms.medicaid_pi.beneficiaries_renewed_total", "geography": {"level": "state", "id": "0400000US06", "vintage": "current"}, "entity": {"name": "person", "role": "medicaid_beneficiary"}, "uuid": "f6053113-5c2f-4832-9f54-e8ba5763f160"} +{"concept": "cms.nursing_home_compare.reported_total_nurse_staffing_hprd_us", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "35d1d31f-ade4-4487-a14c-bd492de01521"} +{"concept": "dol.eta.continued_claims.sa", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "person", "role": "ui_claimant"}, "uuid": "38773338-f84a-466e-94cb-fce55f5e2549"} +{"concept": "dol.eta.initial_claims.sa", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "person", "role": "ui_initial_claimant"}, "uuid": "330ec66b-502c-4ddf-9978-02199bab0055"} +{"concept": "ecb.deposit_facility_rate", "geography": {"level": "country", "id": "EA", "vintage": "current"}, "entity": {"name": "government", "role": "deposit_facility"}, "uuid": "32b335ea-b2dd-4fba-8321-e9751df19318"} +{"concept": "estat.jp.cpi.core_exfreshfood.yoy", "geography": {"level": "country", "id": "JP", "vintage": "current"}, "entity": {"name": "household", "role": "cpi_less_fresh_food"}, "uuid": "0be25641-eae9-45b8-b0c5-218b21e23d61"} +{"concept": "eurostat.ea.hicp.flash.yoy", "geography": {"level": "region", "id": "EA21", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "4eaed6f8-7da3-405d-8293-4b61d5cefc6f"} +{"concept": "eurostat.hicp.all_items_annual_rate.euro_area", "geography": {"level": "region", "id": "EA", "vintage": "current"}, "entity": {"name": "household", "role": "hicp_all_items"}, "uuid": "815299ce-841a-4115-9af2-3b52fc036834"} +{"concept": "eurostat.hicp.all_items_annual_rate.euro_area", "geography": {"level": "region", "id": "EA21", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "74ce2da4-28ed-409a-a0f1-2a7c40dc7caf"} +{"concept": "eurostat.hicp.flash.yoy", "geography": null, "entity": null, "uuid": "1e84eb13-9e80-4dda-9b82-71361b886c1e"} +{"concept": "eurostat.industrial_production.euro_area", "geography": {"level": "region", "id": "EA", "vintage": "current"}, "entity": {"name": "institutional_sector", "role": "industrial_production"}, "uuid": "4994dab0-bc46-4dd0-9bb7-ef716dfc4c75"} +{"concept": "eurostat.retail_trade.volume_mom.euro_area", "geography": {"level": "region", "id": "EA21", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "16b32f44-da0e-43de-bfb2-8b5a2a883b95"} +{"concept": "eurostat.unemployment_rate", "geography": null, "entity": null, "uuid": "2c59f506-873f-45df-b6c5-588c82dcd640"} +{"concept": "eurostat.unemployment_rate.belgium", "geography": {"level": "country", "id": "BE", "vintage": null}, "entity": null, "uuid": "c7f994ce-3d0b-4a28-ba5b-e286237cd14d"} +{"concept": "eurostat.unemployment_rate.euro_area", "geography": {"level": "region", "id": "EA21", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "ec04c486-d8fd-4b1b-9841-884bed8d88c7"} +{"concept": "fed.g17.capacity_utilization.manufacturing", "geography": null, "entity": null, "uuid": "40f48c12-3611-4e57-9482-055e62bf35ae"} +{"concept": "fed.g17.capacity_utilization.total_industry", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "9668ecdb-5f61-422d-a653-8e8956257ade"} +{"concept": "fed.g17.capacity_utilization.total_industry", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "institutional_sector", "role": "total_industry_capacity"}, "uuid": "978252a9-c452-41eb-aece-323340e796fc"} +{"concept": "fed.g17.industrial_production.total_index_mom", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "dbfd5050-e5df-40af-8922-dc8927d6363a"} +{"concept": "fed.g17.industrial_production.total_index_mom", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "institutional_sector", "role": "total_industrial_production"}, "uuid": "d5634fc4-dbe0-4203-a5b7-728708b85a14"} +{"concept": "fed.g17.manufacturing_production_mom", "geography": null, "entity": null, "uuid": "41d26958-709c-44ab-a5ab-b385157db0ac"} +{"concept": "fed.g19.consumer_credit_nonrevolving_annual_rate", "geography": null, "entity": null, "uuid": "bd0643c4-6716-41ae-988d-c985272e38ae"} +{"concept": "fed.g19.consumer_credit_revolving_annual_rate", "geography": null, "entity": null, "uuid": "8a82c6a5-7c83-4ac8-ab5b-7dd6eaf97e8f"} +{"concept": "fed.g19.consumer_credit_total_annual_rate", "geography": null, "entity": null, "uuid": "dac76c03-0630-4859-898d-b4849383d147"} +{"concept": "fns.snap.application_processing_timeliness_rate", "geography": {"level": "state", "id": "0400000US06", "vintage": "current"}, "entity": {"name": "household", "role": "snap_applicant"}, "uuid": "3b195edb-1b2d-4380-8267-43084fe3cbeb"} +{"concept": "fns.snap.overpayment_error_rate", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "ae4cc9d0-e0f7-4de4-97bc-dfcf5afeb90e"} +{"concept": "fns.snap.share_jurisdictions_at_or_above_6pct", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "government", "role": "snap_administering_jurisdiction"}, "uuid": "49b9c81b-935d-4523-a05d-d2000f8064e4"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "d93e4cbb-5810-4a89-80fe-3830df22e057"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US01", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "0902c609-38ed-4b39-94bf-13aa8f5d6cbb"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US02", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "3b0cdfd9-16a4-424b-99ea-dbbcedecefd7"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US04", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "6c19d194-6a6b-4e7a-9fb1-f98ab24e6095"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US05", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "6409e147-6d79-4c75-ba50-7601bd7021dd"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US06", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "93d432d3-0c95-4924-a180-c74622391117"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US08", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "32bc567b-075f-4471-a601-38f7694f1c35"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US09", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "83a60dd2-560a-4f92-9613-83507b73133c"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US10", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "34714371-9105-4e7b-93dd-6f7190d15e55"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US11", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "acfecec9-e491-441e-8df3-02cd52b61191"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US12", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "34e71c1b-05c8-48dd-8625-ddadea1b2445"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US13", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "94d5fcdc-3df6-4b46-bcd0-594dfc907488"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US15", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "f80db1ec-1739-496f-a6f9-47f3c18b280d"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US16", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "5ef9bdf6-62bc-4f50-8a7e-119a1c2f3aad"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US17", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "a90c5f52-5bcf-4920-8b32-c2c236b7ae28"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US18", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "12413650-f9e2-4308-93c7-886ce4f3956e"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US19", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "dc083c88-6ad5-4b84-b7f1-b890ff4e8561"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US20", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "0189b8ff-f12e-40d1-8d8f-3dc7a5e1bffa"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US21", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "6362a941-a7a3-422a-970f-d650c91d18ec"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US22", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "3bab127f-53fe-4a60-a1f0-c6efaaa7dbe2"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US23", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "545502ce-7fa4-44e3-bc00-411f93df5875"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US24", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "75a0857b-f4da-4e5d-8000-33b3c8b8ed6e"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US25", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "c3fc0369-f22e-488d-b5b7-9c75e0de4b1c"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US26", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "e52a49f4-79f6-4c0f-b06d-7c52e788794f"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US27", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "6e782a95-0fee-4f5a-b802-f061ef719fd2"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US28", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "2247c3f5-2393-4d87-a46b-5160cd80ad26"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US29", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "4642db20-cf61-4d04-891d-7d9933c6721e"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US30", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "8022c7e2-4760-40ae-9621-6d1d52d06acc"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US31", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "81f1dc85-41d3-4d12-86cb-6a93338fb4c8"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US32", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "cb4e42fd-c331-4da7-9341-4e546b9ce8cd"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US33", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "ad8be026-4531-46e4-b02d-43d27f93c948"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US34", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "c92381ef-2e2b-493b-ada9-6e9fbd3d2c2d"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US35", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "b5bb3650-78cb-435b-bb66-7f04e38bf470"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US36", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "7ab34230-19e8-4543-999f-2910c1173be5"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US37", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "2834f2ee-480f-476b-8ae6-5f8975cf493b"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US38", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "bb079952-9000-48fd-84da-0e4f38f28964"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US39", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "9fa7ec57-8aad-4680-a867-d6e07a1fde4d"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US40", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "21b36dca-9579-4a20-a947-beaa7afb6b7a"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US41", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "a53e930e-3042-4967-ad17-888f29e653af"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US42", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "e89d9485-e437-40c3-987e-be079dbc45eb"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US44", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "bc31a283-a414-4446-87cb-48d9c8a3db28"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US45", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "9b679b84-0d9f-44fd-86dd-3556bd131e42"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US46", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "4b6ceb75-7ede-4ec3-a1bc-de4bdd8f0dca"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US47", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "6c9ddf7b-f037-4482-89fc-e45728169d88"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US48", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "249143e2-bbd8-46a6-96a8-673dfbf7e14f"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US49", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "25099a0a-db82-4354-aa16-dc0e7d0ee301"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US50", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "1435b3de-9953-4cb5-acd8-ace0df2570ad"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US51", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "28747a0a-dd14-414c-b193-514208ba0a05"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US53", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "39d52b69-8066-4028-81de-a1b938cf0630"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US54", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "6dcd0d3f-5ed9-49d4-93fc-b9ec63c9f699"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US55", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "62000494-b4fe-4a7d-b0d2-013b911e7884"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US56", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "b9d02bc9-68e5-4900-b30e-763f6d760a70"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US66", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "c562b4b3-09ce-45db-b355-cd6c9d412f18"} +{"concept": "fns.snap.total_payment_error_rate", "geography": {"level": "state", "id": "0400000US78", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "55cbe6f5-3aad-473a-8a3d-552b2525eb5f"} +{"concept": "fns.snap.total_persons", "geography": null, "entity": null, "uuid": "748670af-cafa-416e-afcd-b2c303593782"} +{"concept": "fns.snap.underpayment_error_rate", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "household", "role": "snap_participant"}, "uuid": "3fa6210a-33a1-4a3e-bbe5-2af34f29a2b9"} +{"concept": "fns.wic.total_participation", "geography": null, "entity": null, "uuid": "5a76270d-3418-49c8-b510-a08c0df0fc8c"} +{"concept": "nbb.business_barometer.overall", "geography": {"level": "country", "id": "BE", "vintage": null}, "entity": null, "uuid": "a3e8d1fb-5f29-40a8-ab14-a06a53ecb6af"} +{"concept": "nbb.consumer_confidence.indicator", "geography": {"level": "country", "id": "BE", "vintage": null}, "entity": null, "uuid": "64bd2834-d559-4bf2-abff-3eae6fcf6b4b"} +{"concept": "nbb.gdp.flash_qoq", "geography": {"level": "country", "id": "BE", "vintage": null}, "entity": null, "uuid": "03a9d380-e5cf-45d8-b4d8-b3da28aad043"} +{"concept": "ons.cpi.annual_rate", "geography": {"level": "country", "id": "GB", "vintage": "current"}, "entity": {"name": "household", "role": "cpi_all_items"}, "uuid": "8436c65e-a9c6-46db-a2b5-77d2cdfff5c2"} +{"concept": "ons.cpih.annual_rate", "geography": {"level": "country", "id": "GB", "vintage": "current"}, "entity": {"name": "household", "role": "cpih_all_items"}, "uuid": "c7d9ec49-df82-41fa-a184-5cb76379a57e"} +{"concept": "ons.gdp.monthly_growth", "geography": {"level": "country", "id": "GB", "vintage": "current"}, "entity": {"name": "institutional_sector", "role": "monthly_gdp"}, "uuid": "780bdbec-6c59-4714-826f-a3dc66f3b6ab"} +{"concept": "ons.hmrc.paye_payrolled_employees", "geography": {"level": "country", "id": "GB", "vintage": "current"}, "entity": {"name": "person", "role": "paye_payrolled_employee"}, "uuid": "b608f8f5-08be-4535-bae0-98282561532e"} +{"concept": "ons.labour.unemployment_rate", "geography": {"level": "country", "id": "GB", "vintage": "current"}, "entity": {"name": "person", "role": "labour_force"}, "uuid": "15c5a134-1cbe-4990-9efa-97702cd3d4de"} +{"concept": "ons.pusf.j5ii.public_sector_net_borrowing_ex_banks", "geography": {"level": "country", "id": "GB", "vintage": "current"}, "entity": {"name": "government", "role": "public_sector_net_borrowing_ex_banks"}, "uuid": "c5d4619d-a81e-457b-b970-b2e97145caf9"} +{"concept": "ons.retail_sales.volume_mom", "geography": {"level": "country", "id": "GB", "vintage": "current"}, "entity": {"name": "institutional_sector", "role": "retail_sales_volume"}, "uuid": "35138dc0-68d5-40c8-abc8-b1a97789fffa"} +{"concept": "rba.cash_rate_target", "geography": {"level": "country", "id": "AU", "vintage": "current"}, "entity": {"name": "government", "role": "cash_rate_target"}, "uuid": "d79b97a3-405d-4ac5-a47b-7733cd0ac673"} +{"concept": "ssa.ssi.total_recipients", "geography": null, "entity": null, "uuid": "b908460f-bc4c-4416-a7d6-75b43244b752"} +{"concept": "statbel.cpi.headline_yoy", "geography": {"level": "country", "id": "BE", "vintage": null}, "entity": null, "uuid": "99a7178c-5085-4ec2-9714-c4d03f10f184"} +{"concept": "statbel.health_index.yoy", "geography": {"level": "country", "id": "BE", "vintage": null}, "entity": null, "uuid": "8fc8eb86-6f04-4c6b-91ae-ca2a490a175f"} +{"concept": "statcan.36-10-0434-01.all_industries.month_to_month_percent_change", "geography": {"level": "country", "id": "CA", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "987c1860-5642-494e-b9da-4034691430e5"} +{"concept": "statcan.building_permits.total_value_mom.canada", "geography": {"level": "country", "id": "CA", "vintage": "current"}, "entity": {"name": "dwelling", "role": "total_value"}, "uuid": "dcff2e47-6562-406e-95ac-7914478803e1"} +{"concept": "statcan.cpi.all_items_annual_rate.canada", "geography": {"level": "country", "id": "CA", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "51be759e-6118-4e1c-bef0-823e40fd004b"} +{"concept": "statcan.cpi.allitems.yoy", "geography": {"level": "country", "id": "CA", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "23bf4da6-874a-4b29-affb-ee68d03d2dee"} +{"concept": "statcan.employment_insurance.regular_beneficiaries.canada", "geography": {"level": "country", "id": "CA", "vintage": "current"}, "entity": {"name": "person", "role": "ei_beneficiary"}, "uuid": "52fba74f-ea83-4b12-981f-3ca2c581686d"} +{"concept": "statcan.employment_insurance.regular_beneficiaries.canada", "geography": {"level": "country", "id": "CA", "vintage": "current"}, "entity": {"name": "person", "role": "regular_employment_insurance_beneficiary"}, "uuid": "78d00eef-fa0e-4248-ae18-eef6536568d4"} +{"concept": "statcan.gdp_by_industry.monthly_growth", "geography": {"level": "country", "id": "CA", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "166d1a5a-b627-4d1f-9f79-682f28692dd5"} +{"concept": "statcan.lfs.employment_change", "geography": {"level": "country", "id": "CA", "vintage": "current"}, "entity": {"name": "person", "role": "employed"}, "uuid": "552c1dda-abe3-4f6c-86fb-473f455b5e15"} +{"concept": "statcan.lfs.unemployment_rate", "geography": {"level": "country", "id": "CA", "vintage": "current"}, "entity": {"name": "person", "role": "labour_force"}, "uuid": "184e9ddf-aeeb-47eb-9ea4-a60eafa878cc"} +{"concept": "statcan.retail_trade.sales_mom.canada", "geography": {"level": "country", "id": "CA", "vintage": "current"}, "entity": {"name": "institutional_sector", "role": "retail_sales"}, "uuid": "42b102d3-0883-4935-843d-1a8e80434f49"} +{"concept": "statcan.wholesale_trade.sales_mom_exclusions.canada", "geography": {"level": "country", "id": "CA", "vintage": "current"}, "entity": {"name": "institutional_sector", "role": "wholesale_sales_exclusions"}, "uuid": "057c368b-a4e8-42bc-ab29-5b55f4eadfd5"} +{"concept": "statjp.cpi.all_items_annual_rate.japan", "geography": {"level": "country", "id": "JP", "vintage": "current"}, "entity": {"name": "household", "role": "cpi_all_items"}, "uuid": "37ef2549-b8d4-4020-8ebd-a2ceb5fe8401"} +{"concept": "statjp.cpi.tokyo_all_items_annual_rate", "geography": {"level": "country", "id": "JP", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "d26a3e29-c04f-4c86-825d-fba2d306e7af"} +{"concept": "statjp.cpi.tokyo_all_items_yoy", "geography": null, "entity": null, "uuid": "62e78be0-309e-49ba-8604-67abb80f1a03"} +{"concept": "statjp.household_spending.real_yoy.two_or_more_person_households", "geography": {"level": "country", "id": "JP", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "f3c0a92a-1af7-4afc-8578-7ac55076227d"} +{"concept": "statjp.lfs.unemployment_rate.japan", "geography": {"level": "country", "id": "JP", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "d15b52c7-e453-4276-bf9f-0b3362a37ecf"} +{"concept": "treasury.mts.monthly_deficit", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "government", "role": "federal_budget_balance"}, "uuid": "f471eabf-fcff-449b-bac7-60a8561b063b"} +{"concept": "us.bea.core_pce.mom_sa", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "economy", "role": "aggregate"}, "uuid": "5d0b93c4-1c82-4598-869d-010d56860983"} +{"concept": "us.census.housing_starts.total_saar", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "dwelling", "role": "housing_start"}, "uuid": "d542cb8d-46ef-4af5-b7e0-8d38574120c2"} +{"concept": "us.dol.initial_claims.sa", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "person", "role": "ui_claimant"}, "uuid": "1ad67747-d9d8-4ff9-9ebc-58206e658518"} +{"concept": "us.dol.initial_claims.sa", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "person", "role": "ui_initial_claimant"}, "uuid": "830e68bb-9ed6-493c-a8a4-440ba8e53f76"} +{"concept": "us.fed.fomc.target_range_upper", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "government", "role": "federal_funds_target_range_upper"}, "uuid": "a4729a07-d9cd-4736-9473-2f845fd71a2d"} +{"concept": "us.frb.industrial_production.total.mom_sa", "geography": {"level": "country", "id": "0100000US", "vintage": "current"}, "entity": {"name": "institutional_sector", "role": "total_industrial_production"}, "uuid": "b5a74252-bf95-488c-81f6-3deab496c477"} +{"concept": "usaspending.dod.new_prime_awards", "geography": null, "entity": null, "uuid": "8146ecb0-3ef3-49eb-b8a0-e61d15aa276e"} +{"concept": "usaspending.dod.prime_award_obligations", "geography": null, "entity": null, "uuid": "a2649fd9-f382-4003-ba70-01030bc93b5e"} +{"concept": "usaspending.dod.prime_award_transactions", "geography": null, "entity": null, "uuid": "82ff2d7f-04ee-4366-8fe4-b8e919e6efa6"} +{"concept": "usaspending.dod.prime_contract_obligations", "geography": null, "entity": null, "uuid": "11ea0d3d-fab9-42bb-b679-3471d4b60323"} +{"concept": "usaspending.dod.small_business_contract_obligation_share", "geography": null, "entity": null, "uuid": "91e3b668-cb4b-4682-94a5-7ae404a6565d"} +{"concept": "usaspending.dod.unique_prime_contract_recipients", "geography": null, "entity": null, "uuid": "96d17026-b54c-4409-98c9-339b6725fde7"} +{"concept": "usda.fsa.crp.enrolled_acres_total", "geography": {"level": "country", "id": "0100000US", "vintage": null}, "entity": null, "uuid": "4266e63f-dbf3-4c86-be8d-44315dbe32e3"} diff --git a/scripts/build_series_catalog.py b/scripts/build_series_catalog.py index c5f081c..8d3df9e 100644 --- a/scripts/build_series_catalog.py +++ b/scripts/build_series_catalog.py @@ -2,55 +2,94 @@ """Build ledger/series_catalog.json — the canonical series registry. The observation file records facts; this catalog records the SERIES those -facts belong to, one row per (concept, geography, entity) identity — the -same identity dimensions the fact ADR uses — keyed by a UUID that is minted -exactly once and preserved across regenerations. Consumers (Thesis's docket, -bill mappers, permalink surfaces) refer to series by catalog UUID or concept -and never mint parallel identities. - -Family derivation strips period tokens (and nothing else) from -``source_record_id`` and ``measure.concept``, replacing each with ``{P}``. -Two passes strip a segment: a shape pass (the recognized token grammar -below) and a semantic pass (tokens derived from the row's own ``period``, -which catches spellings the grammar has not met yet). Segments that still -contain a four-digit year after both passes are reported in -``suspect_segments`` for curation — flagged, never silently stripped. - -Recognized token grammar (dotted segments): - fy2026 | 2026-05 | 2026_05 | 2026-05-02 | 2026_05_02 | may_2026 | feb_2026 - | q1_2026 | 2026_q1 | week_2026-05-02 | week_2026_05_02 - | week_ending_2026_05_02 | february_to_april_2026 - | after_june_2026 | after_mpc_june_2026 (period-qualifier compounds) - -Release-vintage segments (``first_print``, ``third_estimate``, -``original_submission``) are preserved: collapsing across vintages is a -curation judgment, done by hand-merging catalog rows (the surviving row -keeps its UUID; absorbed spellings move into ``aliases``). The same applies -to concept-spelling drift (``abs.cpi.all_groups.yoy`` vs -``abs.cpi_indicator.allgroups.yoy``): never merged mechanically. Curated -aliases persist across regeneration — the existing catalog's aliases are -unioned with derived ones, and alias matches inherit the existing UUID, so -a hand merge or an upstream concept rename does not remint identity. - -Inputs are pinned: the observation JSONL (digest in ``observations_sha256``) -and the committed docket seed at ``ledger/seeds/thesis_docket_series.json`` -(digest in ``docket_seed_sha256``), which contributes docket-only rows for -Thesis docket series not yet observed. Bare ``--check`` uses both committed -inputs, so CI needs no external files. - -Idempotent: same inputs + same existing catalog -> byte-identical output. -New identities mint fresh UUIDv4s; existing identities keep theirs (looked -up by identity key, then by concept/alias). Unit or cadence conflicts -within one identity are a hard error, never a silent modal pick. +facts belong to, one row per (concept, geography level/id/vintage, entity +name/role) identity — the same identity dimensions the fact ADR uses, +including the geography boundary vintage — keyed by a UUID that is minted +exactly once. Consumers (Thesis's docket, bill mappers, permalink surfaces) +refer to series by catalog UUID or concept and never mint parallel +identities. + +UUID authority is NOT this file: it is the append-only minting ledger at +``ledger/series_uuid_registry.jsonl``. Every binding the catalog has ever +shipped is a line there; the builder inherits UUIDs from the registry (then +from the existing catalog row for identities the registry has not met), and +appends a mint line for every new identity it binds. The catalog embeds the +registry digest (``uuid_registry_sha256``), ``--check`` verifies that every +catalog row agrees with its registry binding, and the registry itself may +only grow: + +* in a git checkout, the working-tree registry must extend the HEAD blob + byte-for-byte (checked on every build and ``--check``); +* in CI, ``--verify-registry-append-only BASE_FILE`` proves the PR keeps the + base branch's registry as an exact byte prefix; +* an identity may change or lose its UUID only through an explicit + ``--allow-remint --remint-note "..."`` run, which appends a supersede line + (``supersedes`` = the replaced UUID, chained per identity) so every remint + is a reviewable event, never a silent regeneration. + +Wholesale remints therefore fail ``--check`` twice over: the reminted +catalog disagrees with the committed registry, and any registry rewrite +breaks the append-only prefix. + +Family derivation replaces period segments of ``source_record_id`` and +``measure.concept`` with ``{P}``. A dotted segment is treated as a period +segment only when it denotes the row's own declared period: either it is a +direct spelling of that period (``period_token_variants``), or it parses as +a calendar window (``fy2026``, ``2026-05``, ``2026_05_02``, ``may_2026``, +``february_to_april_2026``, ``q1_2026``, ``week_ending_2026_05_02``, +``after_mpc_june_2026``) that overlaps the row's period. Date-shaped +segments that do NOT match the row's period — a disjoint window, or an +impossible token like ``2026_13`` — are never stripped: they stay in the +identity and are reported in ``suspect_segments`` for curation. + +Aliases are curated identity statements, not derived data. Observed concept +spellings of one identity become aliases automatically; everything else in +``aliases`` is hand curation and persists across regeneration. Source +labels (``measure.source_concept``) are provenance, recorded per row in +``source_concepts``, and never drive identity inheritance — a source label +may name a different series entirely (a derived share can cite its base +series). Generator versions < 3 mixed source labels into ``aliases``; +purging them (and re-adding the few that are genuine identity statements) +was a one-time curated migration of the committed catalog, reviewed line by +line in the generator-v3 change. + +Alias/concept inheritance is scoped to the SAME (geography, entity): a name +match can heal a rename within one dimension slice but can never move a +UUID across geographies or entities. The one documented exception is +docket-placeholder enrichment: a docket-only row (no observations; entity +unknown; geography absent or matching on level/id) may be claimed by the +first observed rows of that series, which upgrade it in place, keep its +UUID, and register the enriched identity. + +Curated merges (two committed rows that are the same series) are performed +by hand: delete the absorbed row, add its concept to the survivor's +``aliases``, regenerate with ``--allow-remint --remint-note``; the absorbed +identity's observations inherit the survivor's UUID and the registry gains +the supersede line. + +Inputs are pinned: the observation JSONL (``observations_sha256``), the +committed docket seed at ``ledger/seeds/thesis_docket_series.json`` +(``docket_seed_sha256``) — a MISSING seed is a hard error, never a silent +shrink — and the UUID registry (``uuid_registry_sha256``). Bare ``--check`` +uses the committed inputs, so CI needs no external files. + +Idempotent: same inputs + same registry + same existing catalog -> +byte-identical output and an unchanged registry. Unit or cadence conflicts +within one identity are a hard error, never a silent modal pick. UUIDs must +be canonical lowercase UUIDv4 text, and uniqueness is enforced on the +parsed 128-bit value, not the string spelling. """ from __future__ import annotations import argparse +import calendar +import datetime as dt import hashlib import json import pathlib import re +import subprocess import sys import uuid as uuid_module from collections import Counter @@ -59,18 +98,27 @@ OBSERVATIONS = ROOT / "ledger" / "official_observations.jsonl" CATALOG = ROOT / "ledger" / "series_catalog.json" DOCKET_SEED = ROOT / "ledger" / "seeds" / "thesis_docket_series.json" +UUID_REGISTRY = ROOT / "ledger" / "series_uuid_registry.jsonl" -GENERATOR_VERSION = 2 +GENERATOR_VERSION = 3 MONTHS_FULL = [ "january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december", ] +# Exactly twelve, indexable by month-1. "sept" is an accepted alternate +# spelling for parsing only — generator v2 kept it inside this list, which +# silently shifted the derived abbreviations for October-December. MONTHS_ABBREV = [ "jan", "feb", "mar", "apr", "may", "jun", - "jul", "aug", "sep", "sept", "oct", "nov", "dec", + "jul", "aug", "sep", "oct", "nov", "dec", ] -_MONTH_ALT = "|".join(sorted(set(MONTHS_FULL + MONTHS_ABBREV), key=len, reverse=True)) +_MONTH_NUM: dict[str, int] = {} +for _i, _name in enumerate(MONTHS_FULL): + _MONTH_NUM[_name] = _i + 1 + _MONTH_NUM[MONTHS_ABBREV[_i]] = _i + 1 +_MONTH_NUM["sept"] = 9 +_MONTH_ALT = "|".join(sorted(_MONTH_NUM, key=len, reverse=True)) # Docket cadence words -> ledger period types. CADENCE_TO_PERIOD_TYPE = { @@ -91,24 +139,104 @@ "BE": {"level": "country", "id": "BE", "name": None}, } -_PERIOD_SEGMENT = re.compile( - r"^(?:after_(?:[a-z]+_)?)?" # after_june_2026, after_mpc_june_2026 - r"(?:" - r"fy\d{4}" # fy2026 - r"|\d{4}[-_]\d{2}(?:[-_]\d{2})?" # 2026-05, 2026_05, 2026-05-02, 2026_05_02 - r"|(?:%(m)s)_\d{4}" # may_2026, feb_2026 - r"|(?:%(m)s)_to_(?:%(m)s)_\d{4}" # february_to_april_2026 - r"|q[1-4]_\d{4}" # q1_2026 - r"|\d{4}_q[1-4]" # 2026_q1 - r"|week_(?:ending_)?\d{4}[-_]\d{2}[-_]\d{2}" # week_2026-05-02, week_ending_2026_06_06 - r")$" % {"m": _MONTH_ALT} -) - _YEAR_HINT = re.compile(r"(?:19|20)\d{2}") +_FY_RE = re.compile(r"fy(\d{4})") +_NUMERIC_DATE_RE = re.compile(r"(\d{4})[-_](\d{2})(?:[-_](\d{2}))?") +_MONTH_NAME_RE = re.compile(r"(%s)_(\d{4})" % _MONTH_ALT) +_MONTH_RANGE_RE = re.compile(r"(%s)_to_(%s)_(\d{4})" % (_MONTH_ALT, _MONTH_ALT)) +_QUARTER_RE = re.compile(r"q([1-4])_(\d{4})|(\d{4})_q([1-4])") +_WEEK_RE = re.compile(r"week_(?:ending_)?(\d{4})[-_](\d{2})[-_](\d{2})") + + +def _month_span(year: int, month: int) -> tuple[dt.date, dt.date] | None: + if not 1 <= month <= 12: + return None + last = calendar.monthrange(year, month)[1] + return dt.date(year, month, 1), dt.date(year, month, last) + + +def _day(year: int, month: int, day: int) -> dt.date | None: + try: + return dt.date(year, month, day) + except ValueError: + return None + + +def parse_period_token(segment: str) -> tuple | None: + """Parse one dotted segment as a period token. + + Returns ``("fiscal_year", year)`` or ``("span", (start, end))`` with + inclusive ``datetime.date`` bounds, or ``None`` when the segment is not + a well-formed period token. ``after_``-qualified compounds + (``after_june_2026``, ``after_mpc_june_2026``) parse as their base + token. Date-shaped strings that denote no real window — ``2026_13``, + ``2026-02-30`` — return ``None``. + + >>> parse_period_token("fy2026") + ('fiscal_year', 2026) + >>> parse_period_token("2026_05") + ('span', (datetime.date(2026, 5, 1), datetime.date(2026, 5, 31))) + >>> parse_period_token("after_mpc_june_2026") + ('span', (datetime.date(2026, 6, 1), datetime.date(2026, 6, 30))) + >>> parse_period_token("week_ending_2026_06_06") + ('span', (datetime.date(2026, 5, 31), datetime.date(2026, 6, 6))) + >>> parse_period_token("2026_13") is None + True + >>> parse_period_token("m3") is None + True + """ + if segment.startswith("after_"): + rest = segment[len("after_"):] + while rest: + parsed = parse_period_token(rest) + if parsed is not None: + return parsed + if "_" not in rest: + return None + rest = rest.split("_", 1)[1] + return None + m = _FY_RE.fullmatch(segment) + if m: + return ("fiscal_year", int(m.group(1))) + m = _WEEK_RE.fullmatch(segment) + if m: + end = _day(int(m.group(1)), int(m.group(2)), int(m.group(3))) + if end is None: + return None + return ("span", (end - dt.timedelta(days=6), end)) + m = _NUMERIC_DATE_RE.fullmatch(segment) + if m: + year, month = int(m.group(1)), int(m.group(2)) + if m.group(3) is None: + span = _month_span(year, month) + return ("span", span) if span else None + day = _day(year, month, int(m.group(3))) + return ("span", (day, day)) if day else None + m = _MONTH_RANGE_RE.fullmatch(segment) + if m: + first, last = _MONTH_NUM[m.group(1)], _MONTH_NUM[m.group(2)] + year = int(m.group(3)) + if first > last: + return None + start = dt.date(year, first, 1) + end = _month_span(year, last)[1] + return ("span", (start, end)) + m = _MONTH_NAME_RE.fullmatch(segment) + if m: + return ("span", _month_span(int(m.group(2)), _MONTH_NUM[m.group(1)])) + m = _QUARTER_RE.fullmatch(segment) + if m: + quarter = int(m.group(1) or m.group(4)) + year = int(m.group(2) or m.group(3)) + start = dt.date(year, 3 * quarter - 2, 1) + end = _month_span(year, 3 * quarter)[1] + return ("span", (start, end)) + return None + def is_period_segment(segment: str) -> bool: - """Whether one dotted segment matches the period-token grammar. + """Whether one dotted segment parses as a real period token. >>> [is_period_segment(s) for s in ( ... "2026_05", "2026-05", "feb_2026", "week_ending_2026_06_06", @@ -117,17 +245,17 @@ def is_period_segment(segment: str) -> bool: ... )] [True, True, True, True, True, True, True, True, True, True, True] >>> [is_period_segment(s) for s in ( - ... "36-10-0434-01", "g17", "adv44x72", "j5ii", "m3", + ... "36-10-0434-01", "g17", "adv44x72", "j5ii", "m3", "2026_13", ... "first_print", "third_estimate", "original_submission", ... "total_nonfarm_payroll_change", "australia", ... )] - [False, False, False, False, False, False, False, False, False, False] + [False, False, False, False, False, False, False, False, False, False, False] """ - return bool(_PERIOD_SEGMENT.fullmatch(segment)) + return parse_period_token(segment) is not None def period_token_variants(period: dict) -> set[str]: - """Every spelling of ``period`` that may appear as an id segment.""" + """Every direct spelling of ``period`` that may appear as an id segment.""" ptype, value = period.get("type"), period.get("value") tokens: set[str] = set() if value is None: @@ -156,25 +284,113 @@ def period_token_variants(period: dict) -> set[str]: return tokens +def period_descriptor(period: dict | None) -> tuple | None: + """The row period as a comparable descriptor (same shapes as tokens).""" + if not period: + return None + ptype = period.get("type") + value = period.get("value") + if value is None: + return None + value = str(value) + if ptype == "fiscal_year": + if value.isdigit(): + return ("fiscal_year", int(value)) + return None + if ptype == "month": + m = re.fullmatch(r"(\d{4})-(\d{2})", value) + if m: + span = _month_span(int(m.group(1)), int(m.group(2))) + return ("span", span) if span else None + return None + if ptype == "quarter": + m = re.fullmatch(r"(\d{4})-(\d{2})", value) + if m: + year, month = int(m.group(1)), int(m.group(2)) + if not 1 <= month <= 12: + return None + quarter = (month - 1) // 3 + 1 + start = dt.date(year, 3 * quarter - 2, 1) + return ("span", (start, _month_span(year, 3 * quarter)[1])) + return None + if ptype == "week_ending": + m = re.fullmatch(r"(\d{4})-(\d{2})-(\d{2})", value) + if m: + end = _day(int(m.group(1)), int(m.group(2)), int(m.group(3))) + if end is None: + return None + return ("span", (end - dt.timedelta(days=6), end)) + return None + if ptype == "year": + if value.isdigit(): + year = int(value) + return ("span", (dt.date(year, 1, 1), dt.date(year, 12, 31))) + return None + return None + + +def _fiscal_year_span(year: int) -> tuple[dt.date, dt.date]: + # US federal fiscal year, the only fiscal calendar in the ledger today. + return dt.date(year - 1, 10, 1), dt.date(year, 9, 30) + + +def _matches_period(token: tuple, period_desc: tuple) -> bool: + """Whether a parsed token denotes the row's own period (window overlap). + + Fiscal-year tokens match a fiscal-year period only on equal years; mixed + fiscal/calendar comparisons use the US federal fiscal calendar. + """ + if token[0] == "fiscal_year" and period_desc[0] == "fiscal_year": + return token[1] == period_desc[1] + a = _fiscal_year_span(token[1]) if token[0] == "fiscal_year" else token[1] + b = ( + _fiscal_year_span(period_desc[1]) + if period_desc[0] == "fiscal_year" + else period_desc[1] + ) + return a[0] <= b[1] and b[0] <= a[1] + + def family_pattern(identifier: str, period: dict | None = None) -> str: - """Replace period-token segments with ``{P}``. + """Replace segments denoting the row's own period with ``{P}``. + + A segment is stripped only when it is a direct spelling of the row + period or parses to a calendar window overlapping it. Date-shaped + segments disjoint from the row period survive (and are flagged by + ``suspect_segments``), so a statute year, cohort, or edition can never + be silently deleted from an identity. - >>> family_pattern("bls.eci.private_wages_salaries_qoq.2026_q2.first_print") + >>> family_pattern("bls.eci.private_wages_salaries_qoq.2026_q2.first_print", + ... {"type": "quarter", "value": "2026-04"}) 'bls.eci.private_wages_salaries_qoq.{P}.first_print' - >>> family_pattern("census.m3.durable_goods_new_orders_mom.2026_06") + >>> family_pattern("census.m3.durable_goods_new_orders_mom.2026_06", + ... {"type": "month", "value": "2026-06"}) 'census.m3.durable_goods_new_orders_mom.{P}' - >>> family_pattern("boe.bank_rate.after_mpc_june_2026") + >>> family_pattern("boe.bank_rate.after_mpc_june_2026", + ... {"type": "month", "value": "2026-06"}) 'boe.bank_rate.{P}' - >>> family_pattern("ons.labour.unemployment_rate.february_to_april_2026") - 'ons.labour.unemployment_rate.{P}' - >>> family_pattern("abs.cpi.all_groups.yoy") + >>> family_pattern("dol.eta.initial_claims.sa.week_ending_2026_06_06", + ... {"type": "month", "value": "2026-06"}) + 'dol.eta.initial_claims.sa.{P}' + >>> family_pattern("agency.rate.2025_12", {"type": "month", "value": "2026-06"}) + 'agency.rate.2025_12' + >>> family_pattern("abs.cpi.all_groups.yoy", {"type": "month", "value": "2026-05"}) 'abs.cpi.all_groups.yoy' """ derived = period_token_variants(period or {}) - segments = identifier.split(".") - return ".".join( - "{P}" if (is_period_segment(s) or s in derived) else s for s in segments - ) + period_desc = period_descriptor(period) + out = [] + for segment in identifier.split("."): + if segment in derived: + out.append("{P}") + continue + token = parse_period_token(segment) + if token is not None and period_desc is not None: + if _matches_period(token, period_desc): + out.append("{P}") + continue + out.append(segment) + return ".".join(out) def concept_for(pattern: str) -> str: @@ -183,16 +399,28 @@ def concept_for(pattern: str) -> str: def suspect_segments(pattern: str) -> list[str]: - """Post-strip segments that still smell of a date — flagged, not stripped.""" + """Surviving segments that smell of a date — flagged, never stripped. + + Covers both period-shaped segments that contradicted the row's own + period (kept in the identity by ``family_pattern``) and free-form + year-bearing segments. + + >>> suspect_segments("agency.rate.2025_12") + ['2025_12'] + >>> suspect_segments("agency.series.mid2026wave") + ['mid2026wave'] + >>> suspect_segments("bls.eci.private_wages_salaries_qoq.{P}.first_print") + [] + """ return [ s for s in pattern.split(".") - if s != "{P}" and _YEAR_HINT.search(s) + if s != "{P}" and (is_period_segment(s) or _YEAR_HINT.search(s)) ] def _geo_key(geography: dict | None) -> str: g = geography or {} - return f"{g.get('level')}|{g.get('id')}" + return f"{g.get('level')}|{g.get('id')}|{g.get('vintage')}" def _entity_key(entity: dict | None) -> str: @@ -200,6 +428,37 @@ def _entity_key(entity: dict | None) -> str: return f"{e.get('name')}|{e.get('role')}" +def _identity_geography(geography: dict | None) -> dict | None: + if geography is None: + return None + return { + "level": geography.get("level"), + "id": geography.get("id"), + "vintage": geography.get("vintage"), + } + + +def _identity_entity(entity: dict | None) -> dict | None: + if entity is None: + return None + return {"name": entity.get("name"), "role": entity.get("role")} + + +def canonical_uuid_problem(value: object) -> str | None: + """Why ``value`` is not a canonical lowercase UUIDv4 string, else None.""" + if not isinstance(value, str): + return f"uuid {value!r} is not a string" + try: + parsed = uuid_module.UUID(value) + except ValueError: + return f"uuid {value!r} does not parse" + if str(parsed) != value: + return f"uuid {value!r} is not canonical lowercase form ({parsed})" + if parsed.version != 4: + return f"uuid {value} is not UUIDv4" + return None + + def build_identities(rows: list[dict]) -> dict[tuple[str, str, str], dict]: """Group observation rows by (concept, geography, entity) identity.""" identities: dict[tuple[str, str, str], dict] = {} @@ -267,37 +526,53 @@ def _sole(counter: Counter, what: str, key: tuple) -> object: class ExistingCatalog: - """UUID and curated-alias memory from the committed catalog.""" + """Canonical-concept and curated-alias memory from the committed catalog.""" def __init__(self, path: pathlib.Path) -> None: + self.rows: list[dict] = [] self.by_identity: dict[tuple[str, str, str], dict] = {} - self.by_concept: dict[str, list[dict]] = {} - self.by_alias: dict[str, list[dict]] = {} + self.by_dim: dict[tuple[str, str], list[dict]] = {} + self.docket_rows: list[dict] = [] if not path.exists(): return catalog = json.loads(path.read_text(encoding="utf-8")) for row in catalog.get("series", []): + self.rows.append(row) key = ( row["concept"], _geo_key(row.get("geography")), _entity_key(row.get("entity")), ) self.by_identity[key] = row - self.by_concept.setdefault(row["concept"], []).append(row) - for alias in row.get("aliases", []): - self.by_alias.setdefault(alias, []).append(row) + self.by_dim.setdefault((key[1], key[2]), []).append(row) + if row.get("status") == "docket-only": + self.docket_rows.append(row) + + def _row_names(self, row: dict) -> set[str]: + return {row["concept"], *row.get("aliases", [])} + + def match( + self, + key: tuple[str, str, str], + names: set[str], + geography: dict | None, + entity: dict | None, + ) -> dict | None: + """The existing row for this identity. - def match(self, key: tuple[str, str, str], names: set[str]) -> dict | None: - """The existing row for this identity: exact key, else unique name hit.""" + Exact identity key first; else a unique concept/curated-alias hit + WITHIN the same (geography, entity) — names heal renames inside one + dimension slice, never across dimensions; else a unique docket-only + placeholder whose declared dimensions do not contradict the incoming + row (the placeholder-enrichment exception). + """ row = self.by_identity.get(key) if row is not None: return row hits: dict[str, dict] = {} - for name in names: - for row in self.by_concept.get(name, []) + self.by_alias.get(name, []): - hits[row["uuid"]] = row - if len(hits) == 1: - return next(iter(hits.values())) + for candidate in self.by_dim.get((key[1], key[2]), []): + if names & self._row_names(candidate): + hits[candidate["uuid"]] = candidate if len(hits) > 1: raise SystemExit( f"identity {key} matches multiple existing UUIDs via " @@ -305,31 +580,208 @@ def match(self, key: tuple[str, str, str], names: set[str]) -> dict | None: "curate the existing rows (merge or disambiguate aliases) " "before regenerating" ) + if hits: + return next(iter(hits.values())) + placeholder_hits: dict[str, dict] = {} + for candidate in self.docket_rows: + if not names & self._row_names(candidate): + continue + if candidate.get("entity") is not None: + continue + cand_geo = candidate.get("geography") + if cand_geo is not None: + incoming = geography or {} + if (cand_geo.get("level"), cand_geo.get("id")) != ( + incoming.get("level"), + incoming.get("id"), + ): + continue + placeholder_hits[candidate["uuid"]] = candidate + if len(placeholder_hits) > 1: + raise SystemExit( + f"identity {key} matches multiple docket-only placeholders " + f"via names {sorted(names)}: {sorted(placeholder_hits)} — " + "curate the seed/catalog before regenerating" + ) + if placeholder_hits: + return next(iter(placeholder_hits.values())) return None +class UuidRegistry: + """The append-only UUID minting ledger. + + One JSON object per line. A line binds one identity (concept, geography + level/id/vintage, entity name/role) to a UUID. The first line for an + identity is its mint; every later line for the same identity must carry + ``supersedes`` (the previous UUID) and a non-empty ``note``, so identity + changes are chained, explicit events. Lines are never edited or removed; + ``--verify-registry-append-only`` and the git-HEAD prefix check enforce + that the file only grows. Multiple identities may share a UUID (a + curated merge moves an identity onto the survivor's UUID; an enriched + docket placeholder registers its observed identity beside the seed + one) — the catalog still enforces one ROW per UUID. + """ + + def __init__(self, path: pathlib.Path, raw: bytes) -> None: + self.path = path + self.raw = raw + self.entries: list[dict] = [] + self.latest: dict[tuple[str, str, str], dict] = {} + problems: list[str] = [] + for lineno, line in enumerate(raw.decode("utf-8").splitlines(), start=1): + if not line.strip(): + problems.append(f"line {lineno}: blank line") + continue + try: + entry = json.loads(line) + except json.JSONDecodeError as exc: + problems.append(f"line {lineno}: not JSON ({exc})") + continue + if not isinstance(entry, dict) or not isinstance( + entry.get("concept"), str + ): + problems.append(f"line {lineno}: missing concept") + continue + problem = canonical_uuid_problem(entry.get("uuid")) + if problem: + problems.append(f"line {lineno}: {problem}") + continue + key = self.entry_key(entry) + previous = self.latest.get(key) + supersedes = entry.get("supersedes") + if previous is None: + if supersedes is not None: + problems.append( + f"line {lineno}: {key} supersedes {supersedes} but " + "has no prior binding" + ) + else: + if supersedes is None: + problems.append( + f"line {lineno}: {key} re-binds without supersedes " + f"(prior uuid {previous['uuid']})" + ) + elif supersedes != previous["uuid"]: + problems.append( + f"line {lineno}: {key} supersedes {supersedes} but " + f"prior binding is {previous['uuid']}" + ) + if not ( + isinstance(entry.get("note"), str) and entry["note"].strip() + ): + problems.append( + f"line {lineno}: supersede for {key} requires a note" + ) + self.entries.append(entry) + self.latest[key] = entry + if problems: + raise SystemExit( + "uuid registry invalid:\n" + + "\n".join(f" {p}" for p in problems) + ) + + @staticmethod + def entry_key(entry: dict) -> tuple[str, str, str]: + return ( + entry["concept"], + _geo_key(entry.get("geography")), + _entity_key(entry.get("entity")), + ) + + @classmethod + def load(cls, path: pathlib.Path) -> UuidRegistry: + if not path.exists(): + raise SystemExit( + f"uuid registry missing: {path} — the registry is the " + "append-only UUID authority and must exist (create an empty " + "file only when initializing a brand-new catalog)" + ) + return cls(path, path.read_bytes()) + + def binding(self, key: tuple[str, str, str]) -> str | None: + entry = self.latest.get(key) + return entry["uuid"] if entry else None + + @staticmethod + def render_entry(entry: dict) -> str: + ordered = { + "concept": entry["concept"], + "geography": entry.get("geography"), + "entity": entry.get("entity"), + "uuid": entry["uuid"], + } + if entry.get("supersedes") is not None: + ordered["supersedes"] = entry["supersedes"] + ordered["note"] = entry["note"] + return json.dumps(ordered, ensure_ascii=False) + + def stage(self, new_entries: list[dict]) -> None: + """Add entries to the in-memory registry (no file write yet).""" + for entry in new_entries: + self.entries.append(entry) + self.latest[self.entry_key(entry)] = entry + addition = "".join( + self.render_entry(entry) + "\n" for entry in new_entries + ) + self.raw = self.raw + addition.encode("utf-8") + + def write(self) -> None: + self.path.write_bytes(self.raw) + + def sha256(self) -> str: + return hashlib.sha256(self.raw).hexdigest() + + +def _registry_event( + key_concept: str, + geography: dict | None, + entity: dict | None, + row_uuid: str, + supersedes: str | None = None, +) -> dict: + return { + "concept": key_concept, + "geography": _identity_geography(geography), + "entity": _identity_entity(entity), + "uuid": row_uuid, + "supersedes": supersedes, + } + + def build_catalog( observations_path: pathlib.Path, docket_path: pathlib.Path | None, existing: ExistingCatalog, -) -> dict: + registry: UuidRegistry, +) -> tuple[dict, dict]: + """Build the catalog and the identity plan. + + The plan records every registry-affecting outcome: ``mints`` (new + identity bindings to append), ``supersedes`` (identities whose UUID + changes — these require ``--allow-remint``), and ``dropped`` (existing + rows whose UUID would vanish from the catalog — also gated). + """ raw = observations_path.read_bytes() rows = [json.loads(line) for line in raw.decode().splitlines() if line.strip()] identities = build_identities(rows) - # Canonicalize each observed bucket through the existing catalog: a - # curated alias or a unique prior concept/alias hit keeps BOTH the prior - # UUID and the prior canonical concept (curation owns naming; observed - # spellings become aliases). Buckets landing on the same canonical - # identity merge. + # Canonicalize each observed bucket through the existing catalog: an + # exact identity hit, a same-dimension curated-alias hit, or a + # docket-placeholder hit keeps BOTH the prior UUID lineage and the prior + # canonical concept (curation owns naming; observed spellings become + # aliases). Buckets landing on the same canonical identity merge. canonical: dict[tuple[str, str, str], dict] = {} + rekeys: dict[tuple[str, str, str], tuple[str, str, str]] = {} for key in sorted(identities): ident = identities[key] concept, geo_key, entity_key = key - names = ident["concepts"] | ident["source_concepts"] | {concept} - prior = existing.match(key, names) + names = ident["concepts"] | {concept} + prior = existing.match(key, names, ident["geography"], ident["entity"]) canon_concept = prior["concept"] if prior else concept canon_key = (canon_concept, geo_key, entity_key) + if canon_key != key: + rekeys[key] = canon_key bucket = canonical.setdefault( canon_key, { @@ -368,16 +820,46 @@ def build_catalog( bucket["count"] += ident["count"] series: list[dict] = [] - used_uuids: dict[str, tuple] = {} + used_uuids: dict[int, tuple] = {} + plan: dict[str, list] = {"mints": [], "supersedes": [], "dropped": []} def claim_uuid(row_uuid: str, key: tuple) -> str: - if row_uuid in used_uuids: + problem = canonical_uuid_problem(row_uuid) + if problem: + raise SystemExit(f"identity {key}: {problem}") + parsed = uuid_module.UUID(row_uuid).int + if parsed in used_uuids: raise SystemExit( f"UUID collision: {row_uuid} claimed by both " - f"{used_uuids[row_uuid]} and {key} — curate the existing " + f"{used_uuids[parsed]} and {key} — curate the existing " "catalog before regenerating" ) - used_uuids[row_uuid] = key + used_uuids[parsed] = key + return row_uuid + + def resolve_uuid( + canon_key: tuple[str, str, str], + prior: dict | None, + geography: dict | None, + entity: dict | None, + ) -> str: + binding = registry.binding(canon_key) + prior_uuid = prior["uuid"] if prior else None + if prior_uuid and binding and prior_uuid != binding: + # The catalog row disagrees with the registry: an explicit, + # gated remint (the curator edited the row's uuid on purpose). + plan["supersedes"].append( + _registry_event( + canon_key[0], geography, entity, prior_uuid, binding + ) + ) + return prior_uuid + if binding: + return binding + row_uuid = prior_uuid if prior_uuid else str(uuid_module.uuid4()) + plan["mints"].append( + _registry_event(canon_key[0], geography, entity, row_uuid) + ) return row_uuid all_suspects: set[str] = set() @@ -386,13 +868,11 @@ def claim_uuid(row_uuid: str, key: tuple) -> str: concept, _, _ = canon_key prior = bucket["prior"] row_uuid = claim_uuid( - prior["uuid"] if prior else str(uuid_module.uuid4()), canon_key + resolve_uuid(canon_key, prior, bucket["geography"], bucket["entity"]), + canon_key, ) curated_aliases = set(prior.get("aliases", [])) if prior else set() - aliases = sorted( - (bucket["concepts"] | bucket["source_concepts"] | curated_aliases) - - {concept} - ) + aliases = sorted((bucket["concepts"] | curated_aliases) - {concept}) all_suspects.update(bucket["suspects"]) series.append({ "uuid": row_uuid, @@ -405,6 +885,7 @@ def claim_uuid(row_uuid: str, key: tuple) -> str: "entity": bucket["entity"], "sources": sorted(bucket["sources"]), "aliases": aliases, + "source_concepts": sorted(bucket["source_concepts"]), "rid_patterns": sorted(bucket["rid_patterns"]), "first_observed_period": min(bucket["period_values"], default=None), "last_observed_period": max(bucket["period_values"], default=None), @@ -412,18 +893,16 @@ def claim_uuid(row_uuid: str, key: tuple) -> str: }) docket_raw = b"" - if docket_path is not None and docket_path.exists(): + if docket_path is not None: docket_raw = docket_path.read_bytes() docket = json.loads(docket_raw.decode()) - alias_tally = Counter( - alias for row in series for alias in row["aliases"] - ) - claimed: dict[str, dict] = {} + alias_tally = Counter(alias for row in series for alias in row["aliases"]) + claimed_names: set[str] = set() for row in series: - claimed.setdefault(row["concept"], row) + claimed_names.add(row["concept"]) for alias in row["aliases"]: if alias_tally[alias] == 1: - claimed.setdefault(alias, row) + claimed_names.add(alias) for entry in docket["series"]: concept = entry["series"] cadence_word = entry.get("cadence") @@ -432,12 +911,9 @@ def claim_uuid(row_uuid: str, key: tuple) -> str: f"docket cadence {cadence_word!r} for {concept} has no " "period-type mapping; extend CADENCE_TO_PERIOD_TYPE" ) - extras = entry.get("extras") or {} - hit = claimed.get(concept) - if hit is not None: - if concept != hit["concept"] and concept not in hit["aliases"]: - hit["aliases"] = sorted(hit["aliases"] + [concept]) + if concept in claimed_names: continue + extras = entry.get("extras") or {} country = extras.get("country") geography = None if country is not None: @@ -448,12 +924,12 @@ def claim_uuid(row_uuid: str, key: tuple) -> str: "no geography mapping; extend COUNTRY_GEOGRAPHY" ) key = (concept, _geo_key(geography), _entity_key(None)) - prior = existing.match(key, {concept}) + prior = existing.match(key, {concept}, geography, None) row_uuid = claim_uuid( - prior["uuid"] if prior else str(uuid_module.uuid4()), key + resolve_uuid(key, prior, geography, None), key ) curated_aliases = set(prior.get("aliases", [])) if prior else set() - row = { + series.append({ "uuid": row_uuid, "concept": concept, "family_patterns": [family_pattern(concept)], @@ -464,13 +940,13 @@ def claim_uuid(row_uuid: str, key: tuple) -> str: "entity": None, "sources": [], "aliases": sorted(curated_aliases), + "source_concepts": [], "rid_patterns": [], "first_observed_period": None, "last_observed_period": None, "observation_count": 0, - } - series.append(row) - claimed[concept] = row + }) + claimed_names.add(concept) series.sort(key=lambda row: ( row["concept"], @@ -478,23 +954,65 @@ def claim_uuid(row_uuid: str, key: tuple) -> str: _entity_key(row.get("entity")), )) - alias_counts = Counter( - alias for row in series for alias in row["aliases"] - ) + # Absorbed identities: an observed bucket that canonicalized onto a + # different identity moves that identity's registry binding onto the + # surviving UUID (a supersede event) if it pointed elsewhere. + row_uuid_by_key = { + (r["concept"], _geo_key(r.get("geography")), _entity_key(r.get("entity"))): + r["uuid"] + for r in series + } + for original_key, canon_key in sorted(rekeys.items()): + old_binding = registry.binding(original_key) + surviving = row_uuid_by_key.get(canon_key) + if old_binding and surviving and old_binding != surviving: + ident = identities[original_key] + plan["supersedes"].append( + _registry_event( + original_key[0], + ident["geography"], + ident["entity"], + surviving, + old_binding, + ) + ) + + # Existing rows whose UUID would vanish from the catalog entirely. + new_uuids = {r["uuid"] for r in series} + for prior_key, prior_row in sorted(existing.by_identity.items()): + if prior_key in row_uuid_by_key: + continue + if prior_row["uuid"] in new_uuids: + continue # lineage survives on another identity (merge/enrich) + plan["dropped"].append((prior_key, prior_row["uuid"])) + + for kind in ("mints", "supersedes"): + plan[kind].sort( + key=lambda e: (e["concept"], _geo_key(e["geography"]), + _entity_key(e["entity"])) + ) + + alias_counts = Counter(alias for row in series for alias in row["aliases"]) ambiguous_aliases = sorted(a for a, n in alias_counts.items() if n > 1) - return { + catalog = { "comment": ( - "Canonical series catalog. One row per (concept, geography, " - "entity) identity; uuid is minted once and never re-minted " - "(regeneration preserves it by identity, then by concept/alias). " - "Consumers reference series by uuid or concept only. Regenerate " - "with scripts/build_series_catalog.py; verify with --check. " - "Cross-spelling and cross-vintage merges are manual curation: " - "keep the surviving row's uuid, move absorbed spellings to " - "aliases — curated aliases persist across regeneration. Aliases " - "listed in ambiguous_aliases match multiple rows and never " - "drive identity inheritance." + "Canonical series catalog. One row per (concept, geography " + "level/id/vintage, entity) identity. UUID authority is the " + "append-only ledger/series_uuid_registry.jsonl (digest below): " + "a uuid is minted once, inherited from the registry on every " + "regeneration, and changes only through an explicit " + "--allow-remint supersede event recorded there. Consumers " + "reference series by uuid or concept only. Regenerate with " + "scripts/build_series_catalog.py; verify with --check. Aliases " + "are curated identity statements (plus observed spellings of " + "the same identity) and inherit only within one (geography, " + "entity) slice; aliases listed in ambiguous_aliases match " + "multiple rows and never drive inheritance. source_concepts " + "are publisher labels — provenance, never identity. Cross-" + "spelling and cross-vintage merges are manual curation: delete " + "the absorbed row, alias its concept on the survivor, " + "regenerate with --allow-remint." ), "generator_version": GENERATOR_VERSION, "observations_sha256": hashlib.sha256(raw).hexdigest(), @@ -502,10 +1020,12 @@ def claim_uuid(row_uuid: str, key: tuple) -> str: "docket_seed_sha256": ( hashlib.sha256(docket_raw).hexdigest() if docket_raw else None ), + "uuid_registry_sha256": None, "suspect_segments": sorted(all_suspects), "ambiguous_aliases": ambiguous_aliases, "series": series, } + return catalog, plan def render(catalog: dict) -> str: @@ -513,29 +1033,102 @@ def render(catalog: dict) -> str: def validate_uuids(catalog: dict) -> list[str]: - """UUID syntax, version, and global-uniqueness problems.""" + """Canonical-form, version, and parsed-value-uniqueness problems.""" problems: list[str] = [] - seen: dict[str, str] = {} + seen: dict[int, str] = {} for row in catalog.get("series", []): value = row.get("uuid", "") concept = row.get("concept", "?") - try: - parsed = uuid_module.UUID(value) - except (ValueError, AttributeError, TypeError): - problems.append(f"{concept}: uuid {value!r} does not parse") - continue - if parsed.version != 4: - problems.append(f"{concept}: uuid {value} is not UUIDv4") - if value in seen: - problems.append(f"uuid {value} duplicated: {seen[value]} and {concept}") - seen[value] = concept + problem = canonical_uuid_problem(value) + if problem: + problems.append(f"{concept}: {problem}") + if not isinstance(value, str): + continue + try: + parsed = uuid_module.UUID(value).int + except ValueError: + continue + else: + parsed = uuid_module.UUID(value).int + if parsed in seen: + problems.append( + f"uuid {value} duplicates {seen[parsed]} (same 128-bit " + f"value) on {concept}" + ) + else: + seen[parsed] = concept return problems +def registry_agreement_problems( + catalog: dict, registry: UuidRegistry +) -> list[str]: + """Catalog rows whose identity is unbound or disagrees with the registry.""" + problems = [] + for row in catalog.get("series", []): + key = ( + row["concept"], + _geo_key(row.get("geography")), + _entity_key(row.get("entity")), + ) + binding = registry.binding(key) + if binding is None: + problems.append(f"{key}: no registry binding for uuid {row['uuid']}") + elif binding != row["uuid"]: + problems.append( + f"{key}: catalog uuid {row['uuid']} != registry binding " + f"{binding}" + ) + return problems + + +def git_head_bytes(path: pathlib.Path) -> bytes | None: + """The file's committed HEAD content, or None when unavailable.""" + try: + toplevel = subprocess.run( + ["git", "-C", str(path.resolve().parent), "rev-parse", + "--show-toplevel"], + capture_output=True, text=True, check=True, + ).stdout.strip() + rel = path.resolve().relative_to(pathlib.Path(toplevel)).as_posix() + shown = subprocess.run( + ["git", "-C", toplevel, "show", f"HEAD:{rel}"], + capture_output=True, check=True, + ) + return shown.stdout + except (subprocess.CalledProcessError, FileNotFoundError, ValueError): + return None + + +def append_only_problem(base: bytes, current: bytes) -> str | None: + """Why ``current`` is not an append-only extension of ``base``.""" + if current.startswith(base): + return None + return ( + "registry is not an append-only extension of its prior committed " + "content: existing lines were edited or removed" + ) + + +def _check_registry_vs_head(registry: UuidRegistry) -> None: + head = git_head_bytes(registry.path) + if head is None: + print( + "note: no committed HEAD version of the registry to compare " + "against (new file or not a git checkout)", + file=sys.stderr, + ) + return + problem = append_only_problem(head, registry.raw) + if problem: + raise SystemExit(f"uuid registry vs HEAD: {problem}") + + def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--observations", type=pathlib.Path, default=OBSERVATIONS) parser.add_argument("--catalog", type=pathlib.Path, default=CATALOG) + parser.add_argument("--registry", type=pathlib.Path, default=UUID_REGISTRY) parser.add_argument( "--docket", type=pathlib.Path, @@ -550,38 +1143,138 @@ def main(argv: list[str] | None = None) -> int: action="store_true", help="fail if the committed catalog is not current for these inputs", ) + parser.add_argument( + "--allow-remint", + action="store_true", + help=( + "permit identities to change or lose their UUID; every change " + "is recorded as a supersede event in the registry" + ), + ) + parser.add_argument( + "--remint-note", + default=None, + help="required with --allow-remint: why the identity change is right", + ) + parser.add_argument( + "--verify-registry-append-only", + type=pathlib.Path, + default=None, + metavar="BASE_FILE", + help=( + "verify the registry extends BASE_FILE byte-for-byte (CI runs " + "this against the PR base's registry), then exit" + ), + ) args = parser.parse_args(argv) + if args.verify_registry_append_only is not None: + registry = UuidRegistry.load(args.registry) + base = ( + args.verify_registry_append_only.read_bytes() + if args.verify_registry_append_only.exists() + else b"" + ) + problem = append_only_problem(base, registry.raw) + if problem: + sys.stderr.write(problem + "\n") + return 1 + print( + f"registry append-only vs base: ok " + f"({len(registry.entries)} entries)" + ) + return 0 + + if not args.observations.exists(): + raise SystemExit(f"observations missing: {args.observations}") + if args.docket is None or not args.docket.exists(): + raise SystemExit( + f"docket seed missing: {args.docket} — the seed is a pinned " + "input; a catalog regenerated without it silently drops every " + "docket-only series, so this is always a hard error" + ) + registry = UuidRegistry.load(args.registry) + _check_registry_vs_head(registry) + existing = ExistingCatalog(args.catalog) - catalog = build_catalog(args.observations, args.docket, existing) - body = render(catalog) + catalog, plan = build_catalog(args.observations, args.docket, existing, registry) - problems = validate_uuids(catalog) - if problems: - for problem in problems: - sys.stderr.write(f"uuid validation: {problem}\n") - return 1 + identity_changes = [ + f"remint {UuidRegistry.entry_key(e)}: {e['supersedes']} -> {e['uuid']}" + for e in plan["supersedes"] + ] + [ + f"dropped {key}: uuid {row_uuid} would vanish from the catalog" + for key, row_uuid in plan["dropped"] + ] if args.check: + failures: list[str] = [] + if plan["mints"]: + failures.append( + f"{len(plan['mints'])} identities missing from the registry " + "(regenerate to mint them)" + ) + failures.extend(identity_changes) + catalog["uuid_registry_sha256"] = registry.sha256() + body = render(catalog) current = ( args.catalog.read_text(encoding="utf-8") if args.catalog.exists() else "" ) if current != body: - sys.stderr.write( + failures.append( "series_catalog.json is stale for these inputs; regenerate " - "with scripts/build_series_catalog.py\n" + "with scripts/build_series_catalog.py" ) - return 1 - committed_problems = validate_uuids(json.loads(current)) - if committed_problems: - for problem in committed_problems: - sys.stderr.write(f"uuid validation: {problem}\n") + if current: + committed = json.loads(current) + for problem in validate_uuids(committed): + failures.append(f"uuid validation: {problem}") + for problem in registry_agreement_problems(committed, registry): + failures.append(f"registry agreement: {problem}") + if committed.get("docket_seed_sha256") is None: + failures.append("committed catalog has no docket seed digest") + if failures: + for failure in failures: + sys.stderr.write(failure + "\n") return 1 print(f"catalog current: {len(catalog['series'])} series") return 0 + if identity_changes and not args.allow_remint: + for change in identity_changes: + sys.stderr.write(f"identity change requires --allow-remint: " + f"{change}\n") + sys.stderr.write( + "refusing to write: existing identities would change or lose " + "their UUID; rerun with --allow-remint --remint-note '...' if " + "this is deliberate curation\n" + ) + return 1 + if args.allow_remint: + if not (args.remint_note and args.remint_note.strip()): + raise SystemExit("--allow-remint requires --remint-note") + for event in plan["supersedes"]: + event["note"] = args.remint_note.strip() + + problems = validate_uuids(catalog) + if problems: + for problem in problems: + sys.stderr.write(f"uuid validation: {problem}\n") + return 1 + + registry.stage(plan["mints"] + plan["supersedes"]) + catalog["uuid_registry_sha256"] = registry.sha256() + body = render(catalog) + + agreement = registry_agreement_problems(catalog, registry) + if agreement: + for problem in agreement: + sys.stderr.write(f"registry agreement: {problem}\n") + return 1 + + registry.write() args.catalog.write_text(body, encoding="utf-8") observed = sum(1 for r in catalog["series"] if r["status"] == "observed") docket_only = sum(1 for r in catalog["series"] if r["status"] == "docket-only") @@ -589,7 +1282,8 @@ def main(argv: list[str] | None = None) -> int: f"wrote {args.catalog}: {len(catalog['series'])} series " f"({observed} observed, {docket_only} docket-only); " f"suspects={len(catalog['suspect_segments'])}, " - f"ambiguous_aliases={len(catalog['ambiguous_aliases'])}" + f"ambiguous_aliases={len(catalog['ambiguous_aliases'])}, " + f"minted={len(plan['mints'])}, superseded={len(plan['supersedes'])}" ) return 0 diff --git a/tests/test_build_series_catalog.py b/tests/test_build_series_catalog.py index 889a026..e26c464 100644 --- a/tests/test_build_series_catalog.py +++ b/tests/test_build_series_catalog.py @@ -1,12 +1,16 @@ """Regression tests for the series-catalog generator. -The token-grammar cases come from the 2026-08-01 adversarial review of the -first generator: every live period spelling it found unrecognized, plus the -non-period identifiers it confirmed must never be stripped. +Sections mirror the adversarial reviews of the first two generators: the +token-grammar findings (live period spellings, identifiers that must never +be stripped), and the v2 identity findings (wholesale reminting invisible +to --check, cross-dimension alias inheritance, source labels treated as +identity, string-keyed UUID uniqueness, silent seed loss, mismatched +period tokens silently stripped). """ from __future__ import annotations +import doctest import json import pathlib import sys @@ -26,6 +30,7 @@ "2026_06_18", "may_2026", "feb_2026", + "sept_2026", "q1_2026", "2026_q2", "week_2026-06-13", @@ -42,6 +47,8 @@ "adv44x72", "j5ii", "m3", + "2026_13", # date-shaped noise: no such month + "2026-02-30", # date-shaped noise: no such day "first_print", "third_estimate", "original_submission", @@ -60,21 +67,88 @@ def test_non_period_segments_retained(segment: str) -> None: assert not bsc.is_period_segment(segment), segment +def test_abbreviated_months_index_correctly() -> None: + # Generator v2 kept "sept" inside MONTHS_ABBREV, shifting the derived + # abbreviations for October-December. + variants = bsc.period_token_variants({"type": "month", "value": "2026-10"}) + assert "oct_2026" in variants and "october_2026" in variants + variants = bsc.period_token_variants({"type": "month", "value": "2026-12"}) + assert "dec_2026" in variants + + +MONTH_2026_06 = {"type": "month", "value": "2026-06"} + + def test_semantic_pass_strips_unseen_spelling() -> None: - # A spelling the grammar does not know, derivable from the row period. - period = {"type": "month", "value": "2026-06"} + # A spelling derivable from the row period is the period. assert ( - bsc.family_pattern("agency.rate.june_2026", period) + bsc.family_pattern("agency.rate.june_2026", MONTH_2026_06) == "agency.rate.{P}" ) +@pytest.mark.parametrize( + ("identifier", "period", "expected"), + [ + # Finer-grained tokens inside the row's declared period strip. + ( + "dol.eta.initial_claims.sa.week_ending_2026_06_06", + MONTH_2026_06, + "dol.eta.initial_claims.sa.{P}", + ), + ("boe.bank_rate.2026-06-18", MONTH_2026_06, "boe.bank_rate.{P}"), + ( + "boe.bank_rate.after_mpc_june_2026", + MONTH_2026_06, + "boe.bank_rate.{P}", + ), + # A month-range whose window covers the row period strips. + ( + "ons.labour.unemployment_rate.february_to_april_2026", + {"type": "month", "value": "2026-04"}, + "ons.labour.unemployment_rate.{P}", + ), + ( + "bls.eci.private_wages_salaries_qoq.2026_q2.first_print", + {"type": "quarter", "value": "2026-04"}, + "bls.eci.private_wages_salaries_qoq.{P}.first_print", + ), + ("fns.snap.rate.fy2024", {"type": "fiscal_year", "value": 2024}, + "fns.snap.rate.{P}"), + ], +) +def test_tokens_matching_row_period_strip(identifier, period, expected) -> None: + assert bsc.family_pattern(identifier, period) == expected + + +@pytest.mark.parametrize( + ("identifier", "period"), + [ + # Disjoint from the row period: a real date, but not THIS row's. + ("agency.rate.2025_12", MONTH_2026_06), + ("agency.rate.week_ending_2026_01_03", MONTH_2026_06), + ("fns.snap.rate.fy2023", {"type": "fiscal_year", "value": 2024}), + ], +) +def test_mismatched_tokens_flagged_not_stripped(identifier, period) -> None: + pattern = bsc.family_pattern(identifier, period) + assert pattern == identifier # kept in the identity + assert bsc.suspect_segments(pattern) == [identifier.rsplit(".", 1)[1]] + + def test_suspect_segments_flag_but_never_strip() -> None: pattern = bsc.family_pattern("agency.series.mid2026wave") assert pattern == "agency.series.mid2026wave" assert bsc.suspect_segments(pattern) == ["mid2026wave"] +US = {"level": "country", "id": "0100000US", "vintage": "current", + "name": "United States"} +CALIFORNIA = {"level": "state", "id": "0400000US06", "vintage": "current", + "name": "California"} +BRITAIN = {"level": "country", "id": "GB", "vintage": "current", "name": None} + + def _row( concept: str, *, @@ -90,7 +164,7 @@ def _row( "value": 1.0, "observed_at": "2026-06-01", "period": period, - "geography": geography or {"level": "country", "id": "0100000US"}, + "geography": geography or dict(US), "entity": entity or {"name": "economy", "role": "aggregate"}, "measure": { "concept": concept, @@ -102,7 +176,23 @@ def _row( } -def _build(tmp_path: pathlib.Path, rows: list[dict], existing: dict | None = None): +def _registry(tmp_path: pathlib.Path, entries: list[dict] | None = None): + path = tmp_path / "registry.jsonl" + if not path.exists() or entries is not None: + body = "".join( + bsc.UuidRegistry.render_entry(e) + "\n" for e in (entries or []) + ) + path.write_text(body, encoding="utf-8") + return bsc.UuidRegistry.load(path) + + +def _build( + tmp_path: pathlib.Path, + rows: list[dict], + existing: dict | None = None, + registry_entries: list[dict] | None = None, + docket: dict | None = None, +): observations = tmp_path / "obs.jsonl" observations.write_text( "".join(json.dumps(r) + "\n" for r in rows), encoding="utf-8" @@ -110,49 +200,157 @@ def _build(tmp_path: pathlib.Path, rows: list[dict], existing: dict | None = Non catalog_path = tmp_path / "catalog.json" if existing is not None: catalog_path.write_text(json.dumps(existing) + "\n", encoding="utf-8") - catalog = bsc.build_catalog( - observations, None, bsc.ExistingCatalog(catalog_path) + docket_path = None + if docket is not None: + docket_path = tmp_path / "seed.json" + docket_path.write_text(json.dumps(docket), encoding="utf-8") + return bsc.build_catalog( + observations, + docket_path, + bsc.ExistingCatalog(catalog_path), + _registry(tmp_path, registry_entries), ) - return catalog def test_geography_splits_identity(tmp_path: pathlib.Path) -> None: rows = [ _row("fns.snap.error_rate"), - _row( - "fns.snap.error_rate", - geography={"level": "state", "id": "0400000US06"}, - ), + _row("fns.snap.error_rate", geography=dict(CALIFORNIA)), ] - catalog = _build(tmp_path, rows) + catalog, plan = _build(tmp_path, rows) assert len(catalog["series"]) == 2 ids = {(r["geography"] or {}).get("id") for r in catalog["series"]} assert ids == {"0100000US", "0400000US06"} + assert len(plan["mints"]) == 2 + + +def test_geography_vintage_splits_identity(tmp_path: pathlib.Path) -> None: + v1 = {"level": "state", "id": "0400000US06", "vintage": "2020"} + v2 = {"level": "state", "id": "0400000US06", "vintage": "2024"} + rows = [ + _row("fns.snap.error_rate", geography=v1), + _row("fns.snap.error_rate", geography=v2, + period={"type": "month", "value": "2026-06"}), + ] + catalog, _ = _build(tmp_path, rows) + assert len(catalog["series"]) == 2 + forward = [r["geography"]["vintage"] for r in catalog["series"]] + catalog_reversed, _ = _build(tmp_path, list(reversed(rows))) + assert forward == [ + r["geography"]["vintage"] for r in catalog_reversed["series"] + ] + assert sorted(forward) == ["2020", "2024"] -def test_rename_inherits_uuid_via_alias(tmp_path: pathlib.Path) -> None: - first = _build(tmp_path, [_row("bls.cps.unemployment_rate")]) - (tmp_path / "catalog.json").write_text(bsc.render(first), encoding="utf-8") - old_uuid = first["series"][0]["uuid"] - renamed = _build( +def test_cross_geography_name_match_never_inherits( + tmp_path: pathlib.Path, +) -> None: + # v2 repro: a prior US row plus a later California-only observation of + # the same concept silently moved the US UUID to California. + first, _ = _build(tmp_path, [_row("fns.snap.error_rate")]) + us_uuid = first["series"][0]["uuid"] + later, plan = _build( tmp_path, - [_row("bls.cps.jobless_rate", source_concept="bls.cps.unemployment_rate")], + [_row("fns.snap.error_rate", geography=dict(CALIFORNIA))], existing=first, ) - assert renamed["series"][0]["uuid"] == old_uuid + assert later["series"][0]["uuid"] != us_uuid + # The US identity's UUID vanishing is loud, not silent. + assert [key for key, _ in plan["dropped"]] == [ + ("fns.snap.error_rate", "country|0100000US|current", + "economy|aggregate") + ] + + +def test_new_geography_added_incrementally(tmp_path: pathlib.Path) -> None: + # v2 failed this with an ambiguous global name match once a concept had + # several prior geographies. + states = [dict(CALIFORNIA), {"level": "state", "id": "0400000US36", + "vintage": "current"}] + rows = [_row("fns.snap.error_rate")] + [ + _row("fns.snap.error_rate", geography=g) for g in states + ] + first, first_plan = _build(tmp_path, rows) + assert len(first["series"]) == 3 + added = rows + [ + _row( + "fns.snap.error_rate", + geography={"level": "state", "id": "0400000US48", + "vintage": "current"}, + ) + ] + second, plan = _build( + tmp_path, added, existing=first, + registry_entries=first_plan["mints"], + ) + assert len(second["series"]) == 4 + old = {r["uuid"] for r in first["series"]} + assert old < {r["uuid"] for r in second["series"]} + # Only the genuinely new geography mints; the registered three inherit. + assert len(plan["mints"]) == 1 and not plan["dropped"] + assert plan["mints"][0]["geography"]["id"] == "0400000US48" + + +def test_source_concept_never_drives_inheritance( + tmp_path: pathlib.Path, +) -> None: + # v2 repro: agency.rate_b with agency.rate_a's source label silently + # became (or merged into) agency.rate_a. + first, _ = _build( + tmp_path, [_row("agency.rate_a", source_concept="OFFICIAL_SHARED")] + ) + a_uuid = first["series"][0]["uuid"] + assert first["series"][0]["aliases"] == [] + assert first["series"][0]["source_concepts"] == ["OFFICIAL_SHARED"] + solo_b, _ = _build( + tmp_path, + [_row("agency.rate_b", source_concept="OFFICIAL_SHARED")], + existing=first, + ) + assert [r["concept"] for r in solo_b["series"]] == ["agency.rate_b"] + assert solo_b["series"][0]["uuid"] != a_uuid + both, _ = _build( + tmp_path, + [ + _row("agency.rate_a", source_concept="OFFICIAL_SHARED"), + _row("agency.rate_b", source_concept="OFFICIAL_SHARED"), + ], + existing=first, + ) + concepts = [r["concept"] for r in both["series"]] + assert concepts == ["agency.rate_a", "agency.rate_b"] + assert both["series"][0]["uuid"] == a_uuid + assert both["series"][0]["observation_count"] == 1 # no silent merge -def test_curated_alias_persists_and_inherits(tmp_path: pathlib.Path) -> None: - first = _build(tmp_path, [_row("abs.cpi.all_groups.yoy")]) +def test_curated_alias_inherits_within_dimensions( + tmp_path: pathlib.Path, +) -> None: + first, _ = _build(tmp_path, [_row("abs.cpi.all_groups.yoy")]) row = first["series"][0] - row["aliases"] = sorted(row["aliases"] + ["abs.cpi_indicator.allgroups.yoy"]) - merged = _build( + row["aliases"] = ["abs.cpi_indicator.allgroups.yoy"] + merged, plan = _build( tmp_path, [_row("abs.cpi_indicator.allgroups.yoy")], existing=first, ) assert merged["series"][0]["uuid"] == row["uuid"] + assert merged["series"][0]["concept"] == "abs.cpi.all_groups.yoy" assert "abs.cpi_indicator.allgroups.yoy" in merged["series"][0]["aliases"] + assert not plan["dropped"] and not plan["supersedes"] + + +def test_curated_alias_does_not_inherit_across_geography( + tmp_path: pathlib.Path, +) -> None: + first, _ = _build(tmp_path, [_row("boe.bank_rate")]) + first["series"][0]["aliases"] = ["bank_rate.official"] + stolen, _ = _build( + tmp_path, + [_row("bank_rate.official", geography=dict(BRITAIN))], + existing=first, + ) + assert stolen["series"][0]["uuid"] != first["series"][0]["uuid"] def test_ambiguous_alias_match_is_a_hard_error(tmp_path: pathlib.Path) -> None: @@ -161,25 +359,74 @@ def test_ambiguous_alias_match_is_a_hard_error(tmp_path: pathlib.Path) -> None: { "uuid": "11111111-1111-4111-8111-111111111111", "concept": "a.one", - "geography": None, - "entity": None, + "geography": dict(US), + "entity": {"name": "economy", "role": "aggregate"}, "aliases": ["SHARED"], }, { "uuid": "22222222-2222-4222-8222-222222222222", "concept": "a.two", - "geography": None, - "entity": None, + "geography": dict(US), + "entity": {"name": "economy", "role": "aggregate"}, "aliases": ["SHARED"], }, ] } with pytest.raises(SystemExit, match="multiple existing UUIDs"): - _build( - tmp_path, - [_row("a.three", source_concept="SHARED")], - existing=existing, - ) + _build(tmp_path, [_row("SHARED")], existing=existing) + + +def test_docket_placeholder_enrichment_keeps_uuid( + tmp_path: pathlib.Path, +) -> None: + placeholder_uuid = "33333333-3333-4333-8333-333333333333" + existing = { + "series": [ + { + "uuid": placeholder_uuid, + "concept": "census.m3.new_orders", + "geography": {"level": "country", "id": "0100000US", + "name": "United States"}, + "entity": None, + "aliases": [], + "status": "docket-only", + } + ] + } + catalog, plan = _build( + tmp_path, [_row("census.m3.new_orders")], existing=existing + ) + assert len(catalog["series"]) == 1 + row = catalog["series"][0] + assert row["uuid"] == placeholder_uuid + assert row["status"] == "observed" + assert row["entity"] == {"name": "economy", "role": "aggregate"} + assert not plan["dropped"] # lineage survives on the enriched identity + + +def test_docket_placeholder_never_enriches_across_country( + tmp_path: pathlib.Path, +) -> None: + existing = { + "series": [ + { + "uuid": "44444444-4444-4444-8444-444444444444", + "concept": "labour.unemployment_rate", + "geography": {"level": "country", "id": "0100000US", + "name": "United States"}, + "entity": None, + "aliases": [], + "status": "docket-only", + } + ] + } + catalog, _ = _build( + tmp_path, + [_row("labour.unemployment_rate", geography=dict(BRITAIN))], + existing=existing, + ) + observed = [r for r in catalog["series"] if r["status"] == "observed"] + assert observed[0]["uuid"] != "44444444-4444-4444-8444-444444444444" def test_unit_conflict_is_a_hard_error(tmp_path: pathlib.Path) -> None: @@ -195,26 +442,245 @@ def test_unit_conflict_is_a_hard_error(tmp_path: pathlib.Path) -> None: _build(tmp_path, rows) -def test_uuid_validation_catches_bad_and_duplicate() -> None: +def test_uuid_validation_requires_canonical_and_parsed_uniqueness() -> None: + base = "abcd1234-ab12-4ab1-8ab1-abcd1234abcd" catalog = { "series": [ {"uuid": "not-a-uuid", "concept": "a"}, - {"uuid": "33333333-3333-4333-8333-333333333333", "concept": "b"}, - {"uuid": "33333333-3333-4333-8333-333333333333", "concept": "c"}, + {"uuid": base, "concept": "b"}, + {"uuid": base, "concept": "c"}, + {"uuid": base.upper(), "concept": "d"}, + {"uuid": base.replace("-", ""), "concept": "e"}, + {"uuid": "{" + base + "}", "concept": "f"}, ] } problems = bsc.validate_uuids(catalog) assert any("does not parse" in p for p in problems) - assert any("duplicated" in p for p in problems) + # Every non-canonical spelling is rejected AND still counted as the + # same 128-bit value. + assert sum("not canonical lowercase" in p for p in problems) == 3 + assert sum("same 128-bit value" in p for p in problems) == 4 + + +def test_registry_chain_validation(tmp_path: pathlib.Path) -> None: + path = tmp_path / "registry.jsonl" + mint = {"concept": "a.one", "geography": None, "entity": None, + "uuid": "11111111-1111-4111-8111-111111111111"} + rebind = {"concept": "a.one", "geography": None, "entity": None, + "uuid": "22222222-2222-4222-8222-222222222222"} + path.write_text( + json.dumps(mint) + "\n" + json.dumps(rebind) + "\n", encoding="utf-8" + ) + with pytest.raises(SystemExit, match="re-binds without supersedes"): + bsc.UuidRegistry.load(path) + chained = dict(rebind, supersedes=mint["uuid"], note="curated remint") + path.write_text( + json.dumps(mint) + "\n" + json.dumps(chained) + "\n", encoding="utf-8" + ) + registry = bsc.UuidRegistry.load(path) + assert registry.binding(("a.one", "None|None|None", "None|None")) == ( + rebind["uuid"] + ) + wrong_chain = dict( + rebind, supersedes="99999999-9999-4999-8999-999999999999", note="x" + ) + path.write_text( + json.dumps(mint) + "\n" + json.dumps(wrong_chain) + "\n", + encoding="utf-8", + ) + with pytest.raises(SystemExit, match="prior binding is"): + bsc.UuidRegistry.load(path) + no_note = dict(rebind, supersedes=mint["uuid"]) + path.write_text( + json.dumps(mint) + "\n" + json.dumps(no_note) + "\n", encoding="utf-8" + ) + with pytest.raises(SystemExit, match="requires a note"): + bsc.UuidRegistry.load(path) + + +def test_registry_append_only_check() -> None: + base = b'{"concept": "a", "uuid": "x"}\n' + assert bsc.append_only_problem(base, base) is None + assert bsc.append_only_problem(base, base + b'{"more": 1}\n') is None + assert bsc.append_only_problem(base, b'{"edited": true}\n') is not None + assert bsc.append_only_problem(base, b"") is not None + + +SEED = { + "series": [ + { + "series": "abs.labour.unemployment_rate", + "cadence": "monthly", + "extras": {"country": "AU", "targetUnit": "percent"}, + } + ] +} + + +def _repo(tmp_path: pathlib.Path, rows: list[dict], seed: dict = SEED): + (tmp_path / "obs.jsonl").write_text( + "".join(json.dumps(r) + "\n" for r in rows), encoding="utf-8" + ) + (tmp_path / "seed.json").write_text(json.dumps(seed), encoding="utf-8") + (tmp_path / "registry.jsonl").write_text("", encoding="utf-8") + return [ + "--observations", str(tmp_path / "obs.jsonl"), + "--catalog", str(tmp_path / "catalog.json"), + "--docket", str(tmp_path / "seed.json"), + "--registry", str(tmp_path / "registry.jsonl"), + ] + + +def test_main_builds_and_is_byte_idempotent( + tmp_path: pathlib.Path, capsys +) -> None: + argv = _repo(tmp_path, [_row("bls.cps.unemployment_rate")]) + assert bsc.main(argv) == 0 + catalog_1 = (tmp_path / "catalog.json").read_bytes() + registry_1 = (tmp_path / "registry.jsonl").read_bytes() + assert bsc.main(argv) == 0 + assert (tmp_path / "catalog.json").read_bytes() == catalog_1 + assert (tmp_path / "registry.jsonl").read_bytes() == registry_1 + assert bsc.main(argv + ["--check"]) == 0 + assert "catalog current: 2 series" in capsys.readouterr().out + + +def test_main_missing_seed_is_a_hard_error(tmp_path: pathlib.Path) -> None: + argv = _repo(tmp_path, [_row("bls.cps.unemployment_rate")]) + assert bsc.main(argv) == 0 + (tmp_path / "seed.json").unlink() + with pytest.raises(SystemExit, match="docket seed missing"): + bsc.main(argv) + with pytest.raises(SystemExit, match="docket seed missing"): + bsc.main(argv + ["--check"]) + + +def test_main_missing_registry_is_a_hard_error( + tmp_path: pathlib.Path, +) -> None: + argv = _repo(tmp_path, [_row("bls.cps.unemployment_rate")]) + (tmp_path / "registry.jsonl").unlink() + with pytest.raises(SystemExit, match="uuid registry missing"): + bsc.main(argv) + + +def test_main_remint_guard_and_ceremony(tmp_path: pathlib.Path) -> None: + argv = _repo(tmp_path, [_row("bls.cps.unemployment_rate")]) + assert bsc.main(argv) == 0 + catalog = json.loads((tmp_path / "catalog.json").read_text()) + observed = next( + r for r in catalog["series"] if r["status"] == "observed" + ) + old_uuid = observed["uuid"] + observed["uuid"] = "55555555-5555-4555-8555-555555555555" + (tmp_path / "catalog.json").write_text( + json.dumps(catalog, indent=2) + "\n", encoding="utf-8" + ) + registry_before = (tmp_path / "registry.jsonl").read_bytes() + # Refuses without the flag, and writes nothing. + assert bsc.main(argv) == 1 + assert (tmp_path / "registry.jsonl").read_bytes() == registry_before + # A pending remint also fails --check. + assert bsc.main(argv + ["--check"]) == 1 + with pytest.raises(SystemExit, match="requires --remint-note"): + bsc.main(argv + ["--allow-remint"]) + assert ( + bsc.main(argv + ["--allow-remint", "--remint-note", "test remint"]) + == 0 + ) + lines = (tmp_path / "registry.jsonl").read_text().splitlines() + event = json.loads(lines[-1]) + assert event["supersedes"] == old_uuid + assert event["uuid"] == "55555555-5555-4555-8555-555555555555" + assert event["note"] == "test remint" + assert bsc.main(argv + ["--check"]) == 0 + + +def test_main_dropped_identity_requires_allow_remint( + tmp_path: pathlib.Path, +) -> None: + argv = _repo(tmp_path, [_row("bls.cps.unemployment_rate")]) + assert bsc.main(argv) == 0 + (tmp_path / "seed.json").write_text( + json.dumps({"series": []}), encoding="utf-8" + ) + assert bsc.main(argv) == 1 # docket-only row would vanish + assert bsc.main(argv + ["--allow-remint", "--remint-note", "seed cut"]) == 0 + catalog = json.loads((tmp_path / "catalog.json").read_text()) + assert len(catalog["series"]) == 1 + # The binding stays dormant in the registry: no line was removed. + lines = (tmp_path / "registry.jsonl").read_text().splitlines() + assert len(lines) == 2 + assert bsc.main(argv + ["--check"]) == 0 + + +def test_check_rejects_uuid_disjoint_catalog(tmp_path: pathlib.Path) -> None: + # v2 repro: two UUID-disjoint catalogs for the same inputs both passed + # --check. The registry now pins the bindings. + argv = _repo(tmp_path, [_row("bls.cps.unemployment_rate")]) + assert bsc.main(argv) == 0 + body = (tmp_path / "catalog.json").read_text() + catalog = json.loads(body) + for i, row in enumerate(catalog["series"]): + row["uuid"] = f"7777777{i}-7777-4777-8777-777777777777" + (tmp_path / "catalog.json").write_text( + json.dumps(catalog, indent=2) + "\n", encoding="utf-8" + ) + assert bsc.main(argv + ["--check"]) == 1 + + +def test_verify_registry_append_only_mode( + tmp_path: pathlib.Path, capsys +) -> None: + argv = _repo(tmp_path, [_row("bls.cps.unemployment_rate")]) + assert bsc.main(argv) == 0 + registry = tmp_path / "registry.jsonl" + base = tmp_path / "base.jsonl" + base.write_bytes(registry.read_bytes()) + verify = ["--registry", str(registry), + "--verify-registry-append-only", str(base)] + assert bsc.main(verify) == 0 + assert "append-only vs base: ok" in capsys.readouterr().out + # An absent base (registry introduced by this change) is fine. + assert bsc.main( + ["--registry", str(registry), + "--verify-registry-append-only", str(tmp_path / "nope.jsonl")] + ) == 0 + # Any edit to committed lines fails. + lines = registry.read_text().splitlines() + first = json.loads(lines[0]) + first["uuid"] = "66666666-6666-4666-8666-666666666666" + registry.write_text( + "\n".join([json.dumps(first)] + lines[1:]) + "\n", encoding="utf-8" + ) + assert bsc.main(verify) == 1 + + +def test_doctests_pass() -> None: + results = doctest.testmod(bsc) + assert results.failed == 0 def test_committed_catalog_is_current_and_valid() -> None: - """The committed artifact must regenerate byte-identically (seeded) and - carry unique, parseable UUIDv4s.""" - committed = bsc.CATALOG.read_text(encoding="utf-8") - catalog = bsc.build_catalog( - bsc.OBSERVATIONS, bsc.DOCKET_SEED, bsc.ExistingCatalog(bsc.CATALOG) - ) - assert bsc.render(catalog) == committed - assert bsc.validate_uuids(json.loads(committed)) == [] - assert json.loads(committed)["suspect_segments"] == [] + """The committed artifact must regenerate byte-identically from the + committed inputs, agree with the committed registry binding-for-binding, + and carry canonical, parsed-unique UUIDv4s.""" + committed_text = bsc.CATALOG.read_text(encoding="utf-8") + committed = json.loads(committed_text) + registry = bsc.UuidRegistry.load(bsc.UUID_REGISTRY) + catalog, plan = bsc.build_catalog( + bsc.OBSERVATIONS, + bsc.DOCKET_SEED, + bsc.ExistingCatalog(bsc.CATALOG), + registry, + ) + assert not plan["mints"] and not plan["supersedes"] and not plan["dropped"] + catalog["uuid_registry_sha256"] = registry.sha256() + assert bsc.render(catalog) == committed_text + assert bsc.validate_uuids(committed) == [] + assert bsc.registry_agreement_problems(committed, registry) == [] + assert committed["suspect_segments"] == [] + assert committed["docket_seed_sha256"] is not None + assert committed["uuid_registry_sha256"] == registry.sha256() + assert bsc.DOCKET_SEED.exists() + assert len(committed["series"]) == 201 From 45fcc10e44f93d3bba0fef358676b1a2e80a2d7a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 1 Aug 2026 13:48:53 -0400 Subject: [PATCH 05/11] Gate bare-registry rebuilds: missing catalog is an identity event MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-found follow-up to v3: deleting the committed catalog and rebuilding from the registry alone loses curated naming/alias memory, so a previously renamed identity would re-key away from its registry binding and fresh-mint a UUID while the old binding goes dormant — a silent remint that survives catalog/registry agreement AND the append-only checks (the registry only grows). Not reachable today (canonical == derived concept for all 201 rows) but opens after the first rename curation. The builder now refuses to rebuild when the prior catalog has no rows while the registry holds bindings, absent --allow-remint --remint-note. Co-Authored-By: Claude Fable 5 --- scripts/build_series_catalog.py | 12 ++++++++++++ tests/test_build_series_catalog.py | 16 ++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/scripts/build_series_catalog.py b/scripts/build_series_catalog.py index 8d3df9e..7bc5ae8 100644 --- a/scripts/build_series_catalog.py +++ b/scripts/build_series_catalog.py @@ -1206,6 +1206,18 @@ def main(argv: list[str] | None = None) -> int: f"dropped {key}: uuid {row_uuid} would vanish from the catalog" for key, row_uuid in plan["dropped"] ] + if not existing.rows and registry.latest: + # Without the prior catalog, curated naming and aliases are gone: a + # renamed identity re-keys away from its registry binding and would + # fresh-mint while the old binding goes dormant — a silent remint + # that survives every append-only check. Rebuilding from a bare + # registry is therefore itself a gated identity event. + identity_changes.append( + f"prior catalog missing while the registry holds " + f"{len(registry.latest)} bindings — restore " + f"{args.catalog} from git history (rebuilding without curated " + "naming/alias memory can silently re-key renamed identities)" + ) if args.check: failures: list[str] = [] diff --git a/tests/test_build_series_catalog.py b/tests/test_build_series_catalog.py index e26c464..a08c38a 100644 --- a/tests/test_build_series_catalog.py +++ b/tests/test_build_series_catalog.py @@ -684,3 +684,19 @@ def test_committed_catalog_is_current_and_valid() -> None: assert committed["uuid_registry_sha256"] == registry.sha256() assert bsc.DOCKET_SEED.exists() assert len(committed["series"]) == 201 + + +def test_rebuild_without_prior_catalog_is_gated( + tmp_path: pathlib.Path, +) -> None: + # Deleting the committed catalog loses curated naming/alias memory: a + # renamed identity would re-key away from its registry binding and + # fresh-mint silently. The builder must treat a bare-registry rebuild + # as an identity event. + argv = _repo(tmp_path, [_row("bls.cps.unemployment_rate")]) + assert bsc.main(argv) == 0 + (tmp_path / "catalog.json").unlink() + assert bsc.main(argv) == 1 + assert bsc.main( + argv + ["--allow-remint", "--remint-note", "rebuild from registry"] + ) == 0 From 2c31ae4fbda70a7bb2aef53248e5f03c1d39a8a3 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 1 Aug 2026 14:27:28 -0400 Subject: [PATCH 06/11] Close four gaps surfaced by third-review attempt: keys, liveness, audit, push gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first run of the third adversarial review crashed before its REPORT but surfaced four reproducible findings; all fixed: 1. Dimension keys were lossy pipe-joins, so crafted geography/entity values could collide across fields (level "a|b" + id "c" vs "a" + "b|c") and None was indistinguishable from "None". Keys are now JSON-encoded (injective, sortable). No identity moved; the only catalog byte effect besides the new header field is the two Eurostat rows swapping into natural EA < EA21 order. 2. The bare-registry rebuild gate only fired on a zero-row catalog: a PARTIAL deletion still silently re-minted renamed identities. The registry now tracks binding liveness with explicit retire/revive events: dropping a row appends a noted retire line (gated by --allow-remint), a returning identity revives its ORIGINAL uuid automatically, and --check enforces the reverse agreement direction — every live binding's uuid must appear on some catalog row. 3. Window-overlap stripping was silent; a statute/cohort/edition label spelling a window that overlaps the row's own period is mechanically indistinguishable from a period label, so every overlap-based strip is now reported in the catalog's overlap_stripped_segments audit list (currently exactly the eight live segments). 4. The CI registry append-only gate ran only on pull requests; direct pushes now verify against github.event.before, and an unfetchable before-sha (history rewrite) fails loudly instead of skipping. 66 tests; ruff, doctest, --check, byte-idempotence all green; uuids/identities unchanged 201/201. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 16 +- ledger/series_catalog.json | 46 +++-- scripts/build_series_catalog.py | 271 +++++++++++++++++++++++++---- tests/test_build_series_catalog.py | 96 +++++++++- 4 files changed, 361 insertions(+), 68 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 16ab683..df4699c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,14 +40,22 @@ jobs: uv run --locked pytest tests/test_build_series_catalog.py -q # The UUID registry is the append-only minting ledger behind the - # series catalog: a PR may extend it, never edit or truncate it. + # series catalog: a change may extend it, never edit or truncate it. # --check already proves catalog/registry agreement and (via git) # that the working tree extends HEAD; this step closes the - # cross-commit hole by proving the PR head extends the PR BASE. + # cross-commit hole by proving the new head extends the comparison + # base — the PR base for pull requests, the pre-push head for pushes. + # A push whose before-sha is unfetchable (history rewrite) fails + # loudly rather than skipping. - name: Series UUID registry append-only vs base - if: github.event_name == 'pull_request' + if: >- + github.event_name == 'pull_request' || + (github.event_name == 'push' && + github.event.before != '0000000000000000000000000000000000000000') env: - BASE_SHA: ${{ github.event.pull_request.base.sha }} + BASE_SHA: ${{ github.event_name == 'pull_request' + && github.event.pull_request.base.sha + || github.event.before }} run: | git fetch --no-tags --depth=1 origin "$BASE_SHA" if git cat-file -e "$BASE_SHA:ledger/series_uuid_registry.jsonl" \ diff --git a/ledger/series_catalog.json b/ledger/series_catalog.json index 815ca19..ff8244f 100644 --- a/ledger/series_catalog.json +++ b/ledger/series_catalog.json @@ -6,6 +6,16 @@ "docket_seed_sha256": "930424fb48c0be4c9e2ce17d4e0f2a6be886408e814c80324174a7a303fa0271", "uuid_registry_sha256": "c4ccda3f1746ff06cf8cc17dc221ea3a82b7b16a5d719361e2025bc5d7b63356", "suspect_segments": [], + "overlap_stripped_segments": [ + "2026-06-18", + "2026_06_18", + "after_june_2026", + "after_mpc_june_2026", + "february_to_april_2026", + "week_2026-06-13", + "week_2026_06_13", + "week_ending_2026_06_06" + ], "ambiguous_aliases": [], "series": [ { @@ -2446,7 +2456,7 @@ "observation_count": 2 }, { - "uuid": "74ce2da4-28ed-409a-a0f1-2a7c40dc7caf", + "uuid": "815299ce-841a-4115-9af2-3b52fc036834", "concept": "eurostat.hicp.all_items_annual_rate.euro_area", "family_patterns": [ "eurostat.hicp.all_items_annual_rate.euro_area.{P}" @@ -2456,32 +2466,32 @@ "cadence": "month", "geography": { "level": "region", - "id": "EA21", + "id": "EA", "vintage": "current", "name": "Euro area" }, "entity": { - "name": "economy", - "role": "aggregate" + "name": "household", + "role": "hicp_all_items" }, "sources": [ "eurostat" ], "aliases": [ - "eurostat.hicp.all_items_annual_rate.euro_area.june_2026" + "eurostat.hicp.all_items_annual_rate.euro_area.may_2026" ], "source_concepts": [ - "prc_hicp_minr/M.RCH_A.TOTAL.EA21" + "eurostat.hicp.all_items_annual_rate.euro_area" ], "rid_patterns": [ - "eurostat.hicp.all_items_annual_rate.euro_area.{P}.flash" + "eurostat.hicp.all_items_annual_rate.euro_area.{P}.final_first_print" ], - "first_observed_period": "2026-06", - "last_observed_period": "2026-06", + "first_observed_period": "2026-05", + "last_observed_period": "2026-05", "observation_count": 1 }, { - "uuid": "815299ce-841a-4115-9af2-3b52fc036834", + "uuid": "74ce2da4-28ed-409a-a0f1-2a7c40dc7caf", "concept": "eurostat.hicp.all_items_annual_rate.euro_area", "family_patterns": [ "eurostat.hicp.all_items_annual_rate.euro_area.{P}" @@ -2491,28 +2501,28 @@ "cadence": "month", "geography": { "level": "region", - "id": "EA", + "id": "EA21", "vintage": "current", "name": "Euro area" }, "entity": { - "name": "household", - "role": "hicp_all_items" + "name": "economy", + "role": "aggregate" }, "sources": [ "eurostat" ], "aliases": [ - "eurostat.hicp.all_items_annual_rate.euro_area.may_2026" + "eurostat.hicp.all_items_annual_rate.euro_area.june_2026" ], "source_concepts": [ - "eurostat.hicp.all_items_annual_rate.euro_area" + "prc_hicp_minr/M.RCH_A.TOTAL.EA21" ], "rid_patterns": [ - "eurostat.hicp.all_items_annual_rate.euro_area.{P}.final_first_print" + "eurostat.hicp.all_items_annual_rate.euro_area.{P}.flash" ], - "first_observed_period": "2026-05", - "last_observed_period": "2026-05", + "first_observed_period": "2026-06", + "last_observed_period": "2026-06", "observation_count": 1 }, { diff --git a/scripts/build_series_catalog.py b/scripts/build_series_catalog.py index 7bc5ae8..7b60beb 100644 --- a/scripts/build_series_catalog.py +++ b/scripts/build_series_catalog.py @@ -377,20 +377,42 @@ def family_pattern(identifier: str, period: dict | None = None) -> str: >>> family_pattern("abs.cpi.all_groups.yoy", {"type": "month", "value": "2026-05"}) 'abs.cpi.all_groups.yoy' """ + return ".".join( + "{P}" if kind in ("derived", "overlap") else segment + for segment, kind in classify_segments(identifier, period) + ) + + +def classify_segments( + identifier: str, period: dict | None = None +) -> list[tuple[str, str]]: + """Classify each dotted segment: ``derived``, ``overlap``, or ``kept``. + + ``derived`` segments are direct spellings of the row period; + ``overlap`` segments parse to a calendar window overlapping it (these + strip too, but are additionally reported in the catalog's + ``overlap_stripped_segments`` audit list — a table, statute, cohort, or + edition label that happens to spell a window overlapping the row's own + period is mechanically indistinguishable from a period label, so every + such strip stays visible for curation); everything else is ``kept``. + """ derived = period_token_variants(period or {}) period_desc = period_descriptor(period) - out = [] + classified: list[tuple[str, str]] = [] for segment in identifier.split("."): if segment in derived: - out.append("{P}") + classified.append((segment, "derived")) continue token = parse_period_token(segment) - if token is not None and period_desc is not None: - if _matches_period(token, period_desc): - out.append("{P}") - continue - out.append(segment) - return ".".join(out) + if ( + token is not None + and period_desc is not None + and _matches_period(token, period_desc) + ): + classified.append((segment, "overlap")) + continue + classified.append((segment, "kept")) + return classified def concept_for(pattern: str) -> str: @@ -419,13 +441,25 @@ def suspect_segments(pattern: str) -> list[str]: def _geo_key(geography: dict | None) -> str: + """Injective geography key: JSON-encoded, so no delimiter collisions. + + A separator-joined key would let distinct dimension values collide + (level ``a|b`` + id ``c`` vs level ``a`` + id ``b|c``); JSON encoding + escapes everything and keeps None distinct from ``"None"``. + + >>> _geo_key({"level": "a|b", "id": "c"}) == _geo_key( + ... {"level": "a", "id": "b|c"}) + False + >>> _geo_key({"level": "None"}) == _geo_key(None) + False + """ g = geography or {} - return f"{g.get('level')}|{g.get('id')}|{g.get('vintage')}" + return json.dumps([g.get("level"), g.get("id"), g.get("vintage")]) def _entity_key(entity: dict | None) -> str: e = entity or {} - return f"{e.get('name')}|{e.get('role')}" + return json.dumps([e.get("name"), e.get("role")]) def _identity_geography(geography: dict | None) -> dict | None: @@ -484,6 +518,7 @@ def build_identities(rows: list[dict]) -> dict[tuple[str, str, str], dict]: "source_concepts": set(), "rid_patterns": set(), "suspects": set(), + "overlap_strips": set(), "units": Counter(), "period_types": Counter(), "geography": geography, @@ -502,6 +537,12 @@ def build_identities(rows: list[dict]) -> dict[tuple[str, str, str], dict]: ident["rid_patterns"].add(rid_pattern) ident["suspects"].update(suspect_segments(pattern)) ident["suspects"].update(suspect_segments(rid_pattern)) + for identifier in (concept_raw, rid): + ident["overlap_strips"].update( + segment + for segment, kind in classify_segments(identifier, period) + if kind == "overlap" + ) ident["units"][measure.get("unit")] += 1 ident["period_types"][period.get("type")] += 1 source = row.get("source") or {} @@ -611,16 +652,27 @@ def match( class UuidRegistry: """The append-only UUID minting ledger. - One JSON object per line. A line binds one identity (concept, geography - level/id/vintage, entity name/role) to a UUID. The first line for an - identity is its mint; every later line for the same identity must carry - ``supersedes`` (the previous UUID) and a non-empty ``note``, so identity - changes are chained, explicit events. Lines are never edited or removed; - ``--verify-registry-append-only`` and the git-HEAD prefix check enforce - that the file only grows. Multiple identities may share a UUID (a - curated merge moves an identity onto the survivor's UUID; an enriched - docket placeholder registers its observed identity beside the seed - one) — the catalog still enforces one ROW per UUID. + One JSON object per line, binding one identity (concept, geography + level/id/vintage, entity name/role) to a UUID. Four event kinds, chained + per identity and never edited or removed (``--verify-registry-append- + only`` and the git-HEAD prefix check enforce growth-only): + + * mint — the identity's first line; no markers. + * supersede — ``supersedes`` names the previous UUID; ``note`` required. + The binding changes UUID (remint or curated merge). + * retire — ``retired: true`` with the unchanged UUID; ``note`` required. + The identity left the catalog; its binding stays reserved but dormant. + * revive — ``revived: true`` with the unchanged UUID; written + automatically when a retired identity is observed again. + + A LIVE binding (latest event not a retire) must always be represented in + the catalog by its UUID — that is the liveness invariant ``--check`` + enforces, and it is what makes a silent partial rebuild impossible: any + catalog state that loses a live binding's UUID needs an explicit retire + or supersede event to become checkable again. Multiple identities may + share a UUID (a curated merge moves an identity onto the survivor's + UUID; an enriched docket placeholder registers its observed identity + beside the seed one) — the catalog still enforces one ROW per UUID. """ def __init__(self, path: pathlib.Path, raw: bytes) -> None: @@ -650,29 +702,72 @@ def __init__(self, path: pathlib.Path, raw: bytes) -> None: key = self.entry_key(entry) previous = self.latest.get(key) supersedes = entry.get("supersedes") - if previous is None: - if supersedes is not None: + retired = entry.get("retired") + revived = entry.get("revived") + markers = sum( + 1 for marker in (supersedes, retired, revived) + if marker is not None + ) + noted = isinstance(entry.get("note"), str) and entry["note"].strip() + if markers > 1: + problems.append( + f"line {lineno}: {key} mixes supersede/retire/revive " + "markers" + ) + elif previous is None: + if markers: problems.append( - f"line {lineno}: {key} supersedes {supersedes} but " - "has no prior binding" + f"line {lineno}: {key} has no prior binding to " + "supersede/retire/revive" ) - else: - if supersedes is None: + elif supersedes is not None: + if entry.get("retired") or self._is_retired(previous): problems.append( - f"line {lineno}: {key} re-binds without supersedes " - f"(prior uuid {previous['uuid']})" + f"line {lineno}: {key} supersedes a retired binding " + "(revive it first)" ) - elif supersedes != previous["uuid"]: + if supersedes != previous["uuid"]: problems.append( f"line {lineno}: {key} supersedes {supersedes} but " f"prior binding is {previous['uuid']}" ) - if not ( - isinstance(entry.get("note"), str) and entry["note"].strip() - ): + if not noted: problems.append( f"line {lineno}: supersede for {key} requires a note" ) + elif retired is not None: + if retired is not True: + problems.append(f"line {lineno}: retired must be true") + if self._is_retired(previous): + problems.append( + f"line {lineno}: {key} is already retired" + ) + if entry["uuid"] != previous["uuid"]: + problems.append( + f"line {lineno}: retire for {key} must keep uuid " + f"{previous['uuid']}" + ) + if not noted: + problems.append( + f"line {lineno}: retire for {key} requires a note" + ) + elif revived is not None: + if revived is not True: + problems.append(f"line {lineno}: revived must be true") + if not self._is_retired(previous): + problems.append( + f"line {lineno}: {key} revives a live binding" + ) + if entry["uuid"] != previous["uuid"]: + problems.append( + f"line {lineno}: revive for {key} must keep uuid " + f"{previous['uuid']}" + ) + else: + problems.append( + f"line {lineno}: {key} re-binds without supersedes " + f"(prior uuid {previous['uuid']})" + ) self.entries.append(entry) self.latest[key] = entry if problems: @@ -681,6 +776,10 @@ def __init__(self, path: pathlib.Path, raw: bytes) -> None: + "\n".join(f" {p}" for p in problems) ) + @staticmethod + def _is_retired(entry: dict) -> bool: + return entry.get("retired") is True + @staticmethod def entry_key(entry: dict) -> tuple[str, str, str]: return ( @@ -703,6 +802,17 @@ def binding(self, key: tuple[str, str, str]) -> str | None: entry = self.latest.get(key) return entry["uuid"] if entry else None + def is_live(self, key: tuple[str, str, str]) -> bool: + entry = self.latest.get(key) + return entry is not None and not self._is_retired(entry) + + def live_bindings(self) -> list[tuple[tuple[str, str, str], dict]]: + return [ + (key, entry) + for key, entry in sorted(self.latest.items()) + if not self._is_retired(entry) + ] + @staticmethod def render_entry(entry: dict) -> str: ordered = { @@ -714,6 +824,11 @@ def render_entry(entry: dict) -> str: if entry.get("supersedes") is not None: ordered["supersedes"] = entry["supersedes"] ordered["note"] = entry["note"] + elif entry.get("retired") is not None: + ordered["retired"] = True + ordered["note"] = entry["note"] + elif entry.get("revived") is not None: + ordered["revived"] = True return json.dumps(ordered, ensure_ascii=False) def stage(self, new_entries: list[dict]) -> None: @@ -791,6 +906,7 @@ def build_catalog( "source_concepts": set(), "rid_patterns": set(), "suspects": set(), + "overlap_strips": set(), "units": Counter(), "period_types": Counter(), "geography": ident["geography"], @@ -813,6 +929,7 @@ def build_catalog( bucket["source_concepts"] |= ident["source_concepts"] bucket["rid_patterns"] |= ident["rid_patterns"] bucket["suspects"] |= ident["suspects"] + bucket["overlap_strips"] |= ident["overlap_strips"] bucket["units"] += ident["units"] bucket["period_types"] += ident["period_types"] bucket["sources"] |= ident["sources"] @@ -821,7 +938,13 @@ def build_catalog( series: list[dict] = [] used_uuids: dict[int, tuple] = {} - plan: dict[str, list] = {"mints": [], "supersedes": [], "dropped": []} + plan: dict[str, list] = { + "mints": [], + "revives": [], + "supersedes": [], + "retire_pending": [], + "dropped": [], + } def claim_uuid(row_uuid: str, key: tuple) -> str: problem = canonical_uuid_problem(row_uuid) @@ -855,6 +978,18 @@ def resolve_uuid( ) return prior_uuid if binding: + if not registry.is_live(canon_key): + # A retired identity is being observed again: same UUID, + # explicit revive event (no ceremony — resuming an identity + # never changes a binding). + plan["revives"].append( + dict( + _registry_event( + canon_key[0], geography, entity, binding + ), + revived=True, + ) + ) return binding row_uuid = prior_uuid if prior_uuid else str(uuid_module.uuid4()) plan["mints"].append( @@ -863,6 +998,7 @@ def resolve_uuid( return row_uuid all_suspects: set[str] = set() + all_overlap_strips: set[str] = set() for canon_key in sorted(canonical): bucket = canonical[canon_key] concept, _, _ = canon_key @@ -874,6 +1010,7 @@ def resolve_uuid( curated_aliases = set(prior.get("aliases", [])) if prior else set() aliases = sorted((bucket["concepts"] | curated_aliases) - {concept}) all_suspects.update(bucket["suspects"]) + all_overlap_strips.update(bucket["overlap_strips"]) series.append({ "uuid": row_uuid, "concept": concept, @@ -977,16 +1114,45 @@ def resolve_uuid( ) ) - # Existing rows whose UUID would vanish from the catalog entirely. + # Liveness: every live registry binding must keep its UUID somewhere in + # the catalog. A binding that loses it needs an explicit retire (or is + # covered by a planned supersede). This is what makes a partial rebuild + # against a truncated catalog loud instead of silently re-minting. new_uuids = {r["uuid"] for r in series} + superseding_keys = { + UuidRegistry.entry_key(event) for event in plan["supersedes"] + } + retire_keys: set[tuple[str, str, str]] = set() + for key, entry in registry.live_bindings(): + if entry["uuid"] in new_uuids: + continue + if key in superseding_keys: + continue + plan["retire_pending"].append( + dict( + _registry_event( + entry["concept"], + entry.get("geography"), + entry.get("entity"), + entry["uuid"], + ), + retired=True, + ) + ) + retire_keys.add(key) + + # Existing rows whose UUID would vanish without any registry binding to + # retire (handcrafted states only; every written catalog registers). for prior_key, prior_row in sorted(existing.by_identity.items()): if prior_key in row_uuid_by_key: continue if prior_row["uuid"] in new_uuids: continue # lineage survives on another identity (merge/enrich) + if prior_key in retire_keys: + continue plan["dropped"].append((prior_key, prior_row["uuid"])) - for kind in ("mints", "supersedes"): + for kind in ("mints", "revives", "supersedes", "retire_pending"): plan[kind].sort( key=lambda e: (e["concept"], _geo_key(e["geography"]), _entity_key(e["entity"])) @@ -1022,6 +1188,7 @@ def resolve_uuid( ), "uuid_registry_sha256": None, "suspect_segments": sorted(all_suspects), + "overlap_stripped_segments": sorted(all_overlap_strips), "ambiguous_aliases": ambiguous_aliases, "series": series, } @@ -1063,9 +1230,17 @@ def validate_uuids(catalog: dict) -> list[str]: def registry_agreement_problems( catalog: dict, registry: UuidRegistry ) -> list[str]: - """Catalog rows whose identity is unbound or disagrees with the registry.""" + """Catalog/registry disagreements, in both directions. + + Forward: every catalog row's identity must be bound to exactly its UUID. + Reverse (liveness): every live binding's UUID must appear on some + catalog row — a live binding whose UUID is absent means identities were + deleted without a retire/supersede event. + """ problems = [] + row_uuids: set[str] = set() for row in catalog.get("series", []): + row_uuids.add(row.get("uuid")) key = ( row["concept"], _geo_key(row.get("geography")), @@ -1079,6 +1254,12 @@ def registry_agreement_problems( f"{key}: catalog uuid {row['uuid']} != registry binding " f"{binding}" ) + for key, entry in registry.live_bindings(): + if entry["uuid"] not in row_uuids: + problems.append( + f"{key}: live binding {entry['uuid']} has no catalog row — " + "retire or supersede it explicitly" + ) return problems @@ -1202,6 +1383,10 @@ def main(argv: list[str] | None = None) -> int: identity_changes = [ f"remint {UuidRegistry.entry_key(e)}: {e['supersedes']} -> {e['uuid']}" for e in plan["supersedes"] + ] + [ + f"retire {UuidRegistry.entry_key(e)}: live binding {e['uuid']} " + "would leave the catalog" + for e in plan["retire_pending"] ] + [ f"dropped {key}: uuid {row_uuid} would vanish from the catalog" for key, row_uuid in plan["dropped"] @@ -1226,6 +1411,11 @@ def main(argv: list[str] | None = None) -> int: f"{len(plan['mints'])} identities missing from the registry " "(regenerate to mint them)" ) + if plan["revives"]: + failures.append( + f"{len(plan['revives'])} retired identities observed again " + "(regenerate to record the revive events)" + ) failures.extend(identity_changes) catalog["uuid_registry_sha256"] = registry.sha256() body = render(catalog) @@ -1267,7 +1457,7 @@ def main(argv: list[str] | None = None) -> int: if args.allow_remint: if not (args.remint_note and args.remint_note.strip()): raise SystemExit("--allow-remint requires --remint-note") - for event in plan["supersedes"]: + for event in plan["supersedes"] + plan["retire_pending"]: event["note"] = args.remint_note.strip() problems = validate_uuids(catalog) @@ -1276,7 +1466,12 @@ def main(argv: list[str] | None = None) -> int: sys.stderr.write(f"uuid validation: {problem}\n") return 1 - registry.stage(plan["mints"] + plan["supersedes"]) + registry.stage( + plan["mints"] + + plan["revives"] + + plan["supersedes"] + + plan["retire_pending"] + ) catalog["uuid_registry_sha256"] = registry.sha256() body = render(catalog) diff --git a/tests/test_build_series_catalog.py b/tests/test_build_series_catalog.py index a08c38a..67a2edf 100644 --- a/tests/test_build_series_catalog.py +++ b/tests/test_build_series_catalog.py @@ -257,8 +257,11 @@ def test_cross_geography_name_match_never_inherits( assert later["series"][0]["uuid"] != us_uuid # The US identity's UUID vanishing is loud, not silent. assert [key for key, _ in plan["dropped"]] == [ - ("fns.snap.error_rate", "country|0100000US|current", - "economy|aggregate") + ( + "fns.snap.error_rate", + bsc._geo_key(US), + bsc._entity_key({"name": "economy", "role": "aggregate"}), + ) ] @@ -478,9 +481,8 @@ def test_registry_chain_validation(tmp_path: pathlib.Path) -> None: json.dumps(mint) + "\n" + json.dumps(chained) + "\n", encoding="utf-8" ) registry = bsc.UuidRegistry.load(path) - assert registry.binding(("a.one", "None|None|None", "None|None")) == ( - rebind["uuid"] - ) + key = ("a.one", bsc._geo_key(None), bsc._entity_key(None)) + assert registry.binding(key) == rebind["uuid"] wrong_chain = dict( rebind, supersedes="99999999-9999-4999-8999-999999999999", note="x" ) @@ -596,11 +598,15 @@ def test_main_remint_guard_and_ceremony(tmp_path: pathlib.Path) -> None: assert bsc.main(argv + ["--check"]) == 0 -def test_main_dropped_identity_requires_allow_remint( +def test_main_dropped_identity_retires_then_revives( tmp_path: pathlib.Path, ) -> None: argv = _repo(tmp_path, [_row("bls.cps.unemployment_rate")]) assert bsc.main(argv) == 0 + original = json.loads((tmp_path / "catalog.json").read_text()) + docket_uuid = next( + r["uuid"] for r in original["series"] if r["status"] == "docket-only" + ) (tmp_path / "seed.json").write_text( json.dumps({"series": []}), encoding="utf-8" ) @@ -608,12 +614,74 @@ def test_main_dropped_identity_requires_allow_remint( assert bsc.main(argv + ["--allow-remint", "--remint-note", "seed cut"]) == 0 catalog = json.loads((tmp_path / "catalog.json").read_text()) assert len(catalog["series"]) == 1 - # The binding stays dormant in the registry: no line was removed. + # The drop is an explicit retire event; no line was edited or removed. + lines = (tmp_path / "registry.jsonl").read_text().splitlines() + assert len(lines) == 3 + retire = json.loads(lines[-1]) + assert retire["retired"] is True and retire["uuid"] == docket_uuid + assert retire["note"] == "seed cut" + assert bsc.main(argv + ["--check"]) == 0 + # Re-seeding the identity revives the SAME uuid — minted once, ever. + (tmp_path / "seed.json").write_text(json.dumps(SEED), encoding="utf-8") + assert bsc.main(argv) == 0 + catalog = json.loads((tmp_path / "catalog.json").read_text()) + revived_row = next( + r for r in catalog["series"] if r["status"] == "docket-only" + ) + assert revived_row["uuid"] == docket_uuid lines = (tmp_path / "registry.jsonl").read_text().splitlines() - assert len(lines) == 2 + assert len(lines) == 4 + revive = json.loads(lines[-1]) + assert revive["revived"] is True and revive["uuid"] == docket_uuid assert bsc.main(argv + ["--check"]) == 0 +def test_partial_catalog_deletion_cannot_silently_remint( + tmp_path: pathlib.Path, +) -> None: + # The v3-review follow-up: deleting SOME rows (not the whole catalog) + # must not let their identities re-mint silently — every live binding's + # uuid has to stay in the catalog or be explicitly retired. + argv = _repo( + tmp_path, + [_row("bls.cps.unemployment_rate"), _row("bea.real_gdp.saar")], + ) + assert bsc.main(argv) == 0 + catalog = json.loads((tmp_path / "catalog.json").read_text()) + kept = [r for r in catalog["series"] if r["concept"] != "bea.real_gdp.saar"] + removed_uuid = next( + r["uuid"] for r in catalog["series"] + if r["concept"] == "bea.real_gdp.saar" + ) + catalog["series"] = kept + (tmp_path / "catalog.json").write_text( + json.dumps(catalog, indent=2) + "\n", encoding="utf-8" + ) + # The observations still exist, so rebuilding restores the row with its + # registry uuid — but a tampered catalog alone must fail --check on the + # liveness rule before any rebuild. + problems = bsc.registry_agreement_problems( + catalog, bsc.UuidRegistry.load(tmp_path / "registry.jsonl") + ) + assert any(removed_uuid in p and "no catalog row" in p for p in problems) + assert bsc.main(argv + ["--check"]) == 1 + # Rebuild heals: the registry still holds the binding. + assert bsc.main(argv) == 0 + healed = json.loads((tmp_path / "catalog.json").read_text()) + assert any(r["uuid"] == removed_uuid for r in healed["series"]) + + +def test_dimension_keys_are_injective() -> None: + # Delimiter-joined keys let crafted values collide across fields. + assert bsc._geo_key( + {"level": "a|b", "id": "c", "vintage": None} + ) != bsc._geo_key({"level": "a", "id": "b|c", "vintage": None}) + assert bsc._geo_key({"level": "None"}) != bsc._geo_key(None) + assert bsc._entity_key( + {"name": 'x", "y', "role": None} + ) != bsc._entity_key({"name": "x", "role": "y"}) + + def test_check_rejects_uuid_disjoint_catalog(tmp_path: pathlib.Path) -> None: # v2 repro: two UUID-disjoint catalogs for the same inputs both passed # --check. The registry now pins the bindings. @@ -680,6 +748,18 @@ def test_committed_catalog_is_current_and_valid() -> None: assert bsc.validate_uuids(committed) == [] assert bsc.registry_agreement_problems(committed, registry) == [] assert committed["suspect_segments"] == [] + # Every stripped segment that was a window-overlap judgment (rather + # than a direct spelling of the row period) stays auditable. + assert committed["overlap_stripped_segments"] == [ + "2026-06-18", + "2026_06_18", + "after_june_2026", + "after_mpc_june_2026", + "february_to_april_2026", + "week_2026-06-13", + "week_2026_06_13", + "week_ending_2026_06_06", + ] assert committed["docket_seed_sha256"] is not None assert committed["uuid_registry_sha256"] == registry.sha256() assert bsc.DOCKET_SEED.exists() From 971c032f5ce579c45b08f583852d18989e34651f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 1 Aug 2026 15:15:41 -0400 Subject: [PATCH 07/11] Answer third adversarial review: UUID-reuse ban, bijective liveness, full strip audit All nine findings from the third review (BLOCK, CRITICAL) addressed: 1. [CRITICAL] Ordinary mints can no longer reuse ANY bound UUID (parsed 128-bit ownership map at registry load + build-time refusal), so re-keyed identities cannot swap or inherit foreign UUIDs; forged registry mint lines sharing a UUID are rejected at load. The one sanctioned reuse is a retire + succeeds event pair (docket placeholder enrichment), and live bindings' UUIDs must be unique. Liveness/agreement are now IDENTITY-AWARE bijections: each live binding needs a catalog row at exactly its identity with exactly its UUID, both directions. 2. [HIGH] The registry-introduction commit cannot be gated by append-only (no prior registry exists), so the reviewed 201-binding identity->uuid map is pinned as a sha256 anchor in the test suite; changing any binding must edit the anchor in the same diff. New- branch pushes (all-zero before-sha) now fall back to verifying against the canonical branch's registry instead of skipping. 3. [HIGH] Docket name claiming is dimension-scoped: an entry declaring a country is only claimed by rows in that country, duplicate docket series ids are hard errors. 4. [HIGH] Placeholder enrichment refuses a declared, conflicting geography vintage. 5. [HIGH] The strip audit now covers EVERY stripped spelling (stripped_segments replaces overlap_stripped_segments; 27 live entries), and malformed periods (month 13, impossible week dates) are hard errors instead of token factories. 6. [MEDIUM] A remint of a returning retired identity stages revive THEN supersede, and stage() revalidates the entire registry before any bytes are written. 7. [MEDIUM] Registry lines parse with duplicate-member rejection. 8. [MEDIUM] Registry bytes must be LF-only and newline-terminated; .gitattributes pins eol for the catalog, registry, and seed. 9. [LOW] Notes must contain alphanumeric content; no-op supersedes (uuid == supersedes) are rejected. 81 tests; ruff, doctest, --check, byte-idempotence green; identity map unchanged (201/201, minted=0, superseded=0; registry still exactly the 201 bootstrap mints). Co-Authored-By: Claude Fable 5 --- .gitattributes | 7 + .github/workflows/ci.yml | 13 +- ledger/series_catalog.json | 23 +- review-context-brief.md | 97 +++++++ review-context-first-review.md | 12 + review-context-second-review.md | 165 ++++++++++++ review-context-v3-disposition.md | 79 ++++++ scripts/build_series_catalog.py | 417 ++++++++++++++++++++++------- tests/test_build_series_catalog.py | 361 ++++++++++++++++++++++++- 9 files changed, 1056 insertions(+), 118 deletions(-) create mode 100644 review-context-brief.md create mode 100644 review-context-first-review.md create mode 100644 review-context-second-review.md create mode 100644 review-context-v3-disposition.md diff --git a/.gitattributes b/.gitattributes index 52fbd20..da4db6a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,13 @@ # Files whose exact Git and working-tree bytes are hashed by the release chain. ledger/official_observations.jsonl text eol=lf ledger/immutable_prefix.json text eol=lf + +# Series-catalog surfaces: byte digests bind these files (catalog embeds the +# registry digest; the registry's append-only check is a byte-prefix match), +# so line endings must survive any checkout configuration. +ledger/series_catalog.json text eol=lf +ledger/series_uuid_registry.jsonl text eol=lf +ledger/seeds/thesis_docket_series.json text eol=lf releases/manifests/*.json text eol=lf releases/anchors/*.pem text eol=lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df4699c..217343a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,14 +50,21 @@ jobs: - name: Series UUID registry append-only vs base if: >- github.event_name == 'pull_request' || - (github.event_name == 'push' && - github.event.before != '0000000000000000000000000000000000000000') + github.event_name == 'push' env: BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }} + # New-branch pushes have an all-zero before-sha; they must still + # extend the canonical lineage's registry rather than skipping. + FALLBACK_REF: codex/thesis-ledger-facts run: | - git fetch --no-tags --depth=1 origin "$BASE_SHA" + if [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then + git fetch --no-tags --depth=1 origin "$FALLBACK_REF" + BASE_SHA=FETCH_HEAD + else + git fetch --no-tags --depth=1 origin "$BASE_SHA" + fi if git cat-file -e "$BASE_SHA:ledger/series_uuid_registry.jsonl" \ 2>/dev/null; then git show "$BASE_SHA:ledger/series_uuid_registry.jsonl" \ diff --git a/ledger/series_catalog.json b/ledger/series_catalog.json index ff8244f..01f92f9 100644 --- a/ledger/series_catalog.json +++ b/ledger/series_catalog.json @@ -1,18 +1,37 @@ { - "comment": "Canonical series catalog. One row per (concept, geography level/id/vintage, entity) identity. UUID authority is the append-only ledger/series_uuid_registry.jsonl (digest below): a uuid is minted once, inherited from the registry on every regeneration, and changes only through an explicit --allow-remint supersede event recorded there. Consumers reference series by uuid or concept only. Regenerate with scripts/build_series_catalog.py; verify with --check. Aliases are curated identity statements (plus observed spellings of the same identity) and inherit only within one (geography, entity) slice; aliases listed in ambiguous_aliases match multiple rows and never drive inheritance. source_concepts are publisher labels — provenance, never identity. Cross-spelling and cross-vintage merges are manual curation: delete the absorbed row, alias its concept on the survivor, regenerate with --allow-remint.", + "comment": "Canonical series catalog. One row per (concept, geography level/id/vintage, entity) identity. UUID authority is the append-only ledger/series_uuid_registry.jsonl (digest below): a uuid is minted once, inherited from the registry on every regeneration, and changes only through explicit, chained supersede/retire/revive/succeeds events recorded there (live bindings and rows stay in bijection). Consumers reference series by uuid or concept only. Regenerate with scripts/build_series_catalog.py; verify with --check. Aliases are curated identity statements (plus observed spellings of the same identity) and inherit only within one (geography, entity) slice; aliases listed in ambiguous_aliases match multiple rows and never drive inheritance. source_concepts are publisher labels — provenance, never identity. Cross-spelling and cross-vintage merges are manual curation: delete the absorbed row, alias its concept on the survivor, regenerate with --allow-remint.", "generator_version": 3, "observations_sha256": "63127ff427a4aa3884f54dd1ee070ab631c15ebb38f23fdc9095a45e9204109f", "observation_rows": 168, "docket_seed_sha256": "930424fb48c0be4c9e2ce17d4e0f2a6be886408e814c80324174a7a303fa0271", "uuid_registry_sha256": "c4ccda3f1746ff06cf8cc17dc221ea3a82b7b16a5d719361e2025bc5d7b63356", "suspect_segments": [], - "overlap_stripped_segments": [ + "stripped_segments": [ + "2026-05", + "2026-06", "2026-06-18", + "2026-07", + "2026_05", + "2026_06", "2026_06_18", + "2026_q2", "after_june_2026", "after_mpc_june_2026", + "april_2026", + "feb_2026", "february_to_april_2026", + "fy2024", + "fy2025", + "june_2026", + "may_2026", + "q1_2026", "week_2026-06-13", + "week_2026-06-20", + "week_2026-06-27", + "week_2026-07-04", + "week_2026-07-11", + "week_2026-07-18", + "week_2026-07-25", "week_2026_06_13", "week_ending_2026_06_06" ], diff --git a/review-context-brief.md b/review-context-brief.md new file mode 100644 index 0000000..e888756 --- /dev/null +++ b/review-context-brief.md @@ -0,0 +1,97 @@ +# Third adversarial review brief — series-catalog v3 (commits 3cb0c8a + 45fcc10 + 2c31ae4) + +You are the third adversarial reviewer of PolicyEngine/ledger PR #128 on +branch thesis-series-catalog. The first two reviews returned BLOCK; v3 +claims to answer every finding. Your job is to try to break v3. + +Materials (untracked briefing files in this worktree root — ignore them in +any cleanliness assessment, do not commit or delete them): +- review-context-first-review.md (first BLOCK review) +- review-context-second-review.md (second BLOCK review — the v3 spec) +- review-context-v3-disposition.md (the disposition you are auditing) + +Scope: the v3 change = commits 3cb0c8a + 45fcc10 + 2c31ae4 (diff 2859ecb..2c31ae4, the branch head): +scripts/build_series_catalog.py, tests/test_build_series_catalog.py, +ledger/series_catalog.json, ledger/series_uuid_registry.jsonl (new), +.github/workflows/ci.yml. + +A previous run of this review crashed before finishing; its four findings +(lossy pipe-joined keys; partial-catalog silent re-mint; silent overlap +strips; PR-only CI append gate) are claimed FIXED in 2c31ae4 — re-verify +each fix adversarially as part of Section 2. + +RUNTIME BUDGET (hard): do NOT launch the full repository test suite (uv run +pytest with no path); it exceeds your session budget and stalled your +predecessor — CI covers it. Run the focused suite +(tests/test_build_series_catalog.py), ruff on the two changed files, +doctest, --check, and targeted experiments only. Prefer many small commands +over any long-running one; nothing you start should run longer than ~90 +seconds. + +Required work, in order: + +1. VERIFY EVERY DISPOSITION CLAIM INDEPENDENTLY. Do not trust the + disposition's validation record — rerun it: pytest, ruff, doctest, + --check, byte idempotence (catalog AND registry), the 201/201 UUID + continuity claim from 2859ecb, the 6ab4fbe catalog-swap repro, missing + seed both modes, uuid spelling variants, registry line edits vs --check + and vs --verify-registry-append-only, the remint ceremony (refuse / + note required / supersede line appended / chain validates / check green + after), the dropped-identity guard, and each healed case (FNS 54-way + incl. national count 2, BoE count 2, M3 observed, Eurostat flash/final + separation, 46 docket-only, zero suspects, empty ambiguous_aliases). + +2. ATTACK THE NEW MECHANICS. At minimum: + - Registry semantics: can you construct a registry state that validates + but lets an identity change UUIDs silently? Chain forgery? A mint + line appended for an existing identity under a cosmetically different + key spelling (geography name/vintage variations, entity null vs + missing)? Shared-uuid states that corrupt the catalog? + - Append-only enforcement: bypasses via git states (clean tree after + committing a rewritten registry — what catches it and when), the CI + step's base-sha choice, shallow clones, the file not existing at + base, trailing-newline and encoding edge cases in the byte-prefix + comparison. + - The --allow-remint ceremony: can a remint slip through without a + supersede line? Can supersede lines be written that misdescribe what + happened? Does --check really fail for every pending identity change? + - Same-dimension scoping: cross-geography/entity theft via crafted + aliases, vintage-only mismatches, null-vs-present geography, the + docket-placeholder enrichment exception (cross-country, entity + present, multiple placeholders). + - Interval-overlap stripping: tokens that overlap the row period but + are semantically NOT period labels (statute years, cohort years, + table editions shaped like dates); fiscal-year edge cases (fy token + on month rows, non-US fiscal conventions); quarter/month boundary + overlaps; impossible tokens; the suspect-flagging contract. + - Canonical UUID enforcement: any path where a non-canonical or + duplicate-by-value uuid enters the catalog or registry. + - The live artifact: spot-check rows against raw observations again + (your predecessor's 12-row table), confirm no identity moved + geography/entity/vintage vs 2859ecb, confirm the two new curated + aliases are the ONLY alias additions and are justified, confirm + source_concepts fields are faithful. + +3. JUDGE THE RESIDUALS the disposition declares deliberate: the three + alias-linked pairs left separate; dormant registry bindings after + drops; enrichment minting a second binding for the same uuid; the + one-time offline migration instead of in-code scrub. Are any of these + exploitable or dishonest rather than merely conservative? + +Rules: read-only with respect to tracked files — run all mutating +experiments on copies under /tmp, never on this worktree's tracked files; +leave `git status` clean apart from the four review-context-*.md files. +Use python3/uv, ruff, pytest as the repo does. Do not push, do not comment +on GitHub; your only output is the report. + +Output format — end your final message with exactly this structure: + +REPORT +verdict: MERGE | BLOCK +risk: LOW | MEDIUM | HIGH | CRITICAL +findings: (ranked, each with severity, file:line evidence, and a concrete +reproduction; empty section allowed only with verdict MERGE) +disposition-audit: (per disposition claim: CONFIRMED | REFUTED | PARTIAL, +one line each) +residuals-judgment: (per declared residual: ACCEPTABLE | UNACCEPTABLE + why) +validation-record: (commands you ran and their outcomes) diff --git a/review-context-first-review.md b/review-context-first-review.md new file mode 100644 index 0000000..7c8dcfc --- /dev/null +++ b/review-context-first-review.md @@ -0,0 +1,12 @@ +An adversarial sol review of the first commit returned **BLOCK** with six findings; the two follow-ups respond to all of them. Review highlights and dispositions: + +1. **Untracked docket seed / bare `--check` broken** → the seed is now committed at `ledger/seeds/thesis_docket_series.json` and digest-bound in the header (`docket_seed_sha256`); bare `--check` covers the full input set and runs in CI. +2. **29 live period spellings unrecognized, already splitting UUIDs** (`2026_06`, `2026_06_18`, `feb_2026`, `week_ending_…`, `week_2026_06_13`, `after_june_2026`, `after_mpc_june_2026`, `february_to_april_2026`) → the token grammar covers every one, plus a semantic pass derives expected tokens from each row's own `period`, and any surviving year-bearing segment lands in `suspect_segments` (committed catalog: zero). The M3 and initial-claims duplicate UUIDs heal; BoE's two rate spellings merge. +3. **Rename/curation/collision loses or remints identity; `--check` trusts blindly** → UUIDs inherit by identity key, then by unique concept/alias match; curated aliases persist across regeneration and keep the prior row's canonical concept; ambiguous matches and UUID collisions are hard errors; `--check` validates UUID syntax/version/uniqueness. +4. **Concept-only key collapses 54 geography subseries (vs the fact-identity ADR)** → identity is now (concept, geography, entity): FNS error rates split into national + per-state rows; Eurostat flash (EA21/economy) and final (EA/household) separate. 141 rows → 201. +5. **Modal masking** → `_modal` is gone; unit/cadence conflicts within an identity are hard errors, never a silent pick. +6. **Provenance** → header binds observations digest + seed digest; the PR-body claim about field derivation is corrected: UUIDs are minted state preserved across regenerations, not derived from inputs; docket rows without a declared country carry null geography rather than a fabricated default. + +Regression tests (`tests/test_build_series_catalog.py`, 34 cases incl. the review's full token table) + CI step added. A focused re-review of the v2 diff runs next; merge only on green + agreement. + +🤖 Generated with [Claude Code](https://claude.com/claude-code) diff --git a/review-context-second-review.md b/review-context-second-review.md new file mode 100644 index 0000000..7e40493 --- /dev/null +++ b/review-context-second-review.md @@ -0,0 +1,165 @@ +# Focused re-review — series-catalog v2 + +**Reviewed range:** `9b4329e..HEAD` (`6ab4fbe`, `2859ecb`) +**Verdict:** **BLOCK** +**Risk:** **CRITICAL** — the committed artifact has discarded every previously minted UUID, the checker accepts multiple UUID-disjoint catalogs for the same inputs, and the new alias fallback can move or merge identities across the dimensions that are supposed to define them. + +The seed, current period spellings, FNS geography split, BoE merge, M3 normalization, observed-row unit/cadence rejection, and ordinary stale-catalog CI check all improved. Those passes do not offset the identity failures below. + +## Ranked findings + +### 1. [CRITICAL] The commits wholesale remint the registry, and `--check` cannot detect the remint + +The catalog promises that a UUID is “minted once and never re-minted” (`ledger/series_catalog.json:2`; also `scripts/build_series_catalog.py:5-7,41-44`). The committed history does the opposite: + +- From `9b4329e` to `6ab4fbe`, the builder's own identity projection `(concept, geography level/id, entity name/role)` has 116 common identities; **0 of 116 UUIDs survive**. The total UUID-value intersection is **0 of 141**. +- From `6ab4fbe` to `2859ecb`, all 201 identity keys are unchanged and **0 of 201 UUIDs survive**. The catalog diff consists only of 201 removed UUID lines and 201 added UUID lines; observations hash, seed hash, row counts, concepts, and metadata are unchanged. +- Example: unchanged ABS building approvals is `0a67f2eb-…` at `9b4329e:ledger/series_catalog.json:8`, `e344ff46-…` at `6ab4fbe:ledger/series_catalog.json:18`, and `efbb2901-…` now (`ledger/series_catalog.json:18`). The unchanged docket-only `abs.labour.unemployment_rate` similarly moves from `843e5bab-…` (`9b4329e:ledger/series_catalog.json:168-183`) to `443cb1bf-…` (`ledger/series_catalog.json:183-198`). +- The merged BoE row now uses `8fb890ac-…` (`ledger/series_catalog.json:1619-1650`), preserving neither prior BoE identity. M3 orders now uses `294d82e8-…` (`ledger/series_catalog.json:1805-1835`), preserving neither the prior docket nor observed UUID. + +This is not forced by the final algorithm. Running the HEAD builder against a temporary copy of the `6ab4fbe` catalog reports `catalog current: 201 series` and exits 0; running it against HEAD also exits 0. Thus two completely UUID-disjoint 201-row catalogs are accepted for the same observation and seed bytes. + +The reason is circular state: the file under check first supplies the prior UUIDs (`scripts/build_series_catalog.py:555-557`, reused at `:387-390`), and the generated result is then compared with that same file (`:565-577`). Syntax/version validation cannot establish historical continuity. This directly fails disposition 3 and means the new CI step would not have caught the actual 201-ID remint in `2859ecb`. + +### 2. [HIGH] Alias fallback bypasses geography/entity identity and omits geography vintage + +An exact identity match uses the full derived key (`scripts/build_series_catalog.py:290-294`), but the fallback searches global concept and alias indexes without filtering candidates to the incoming geography/entity (`:295-300`). The matched prior UUID is then applied to the incoming geography/entity (`:329-332,387-390`). + +Adversarial results: + +- With one prior US row and a later California-only row of the same name, the California row silently inherits the US UUID: the UUID moves to a different geography. +- If both old US and new California rows are present, `claim_uuid` eventually errors because both claim the same raw UUID. That prevents corruption but also prevents an ordinary new geography. +- Once a concept has several prior geographies (for example the 54 FNS rows), adding a new geography fails earlier as an ambiguous global name match. + +The fallback is useful for enriching a docket-only placeholder whose entity is not yet known, but it is too broad for already observed identities. There is no regression test for geography movement or incremental geography addition; `tests/test_build_series_catalog.py:119-130` only builds two geographies from an empty catalog. + +The key is also not actually the full geography object claimed by the disposition. `_geo_key` includes only `level|id` (`scripts/build_series_catalog.py:193-195`), while the observation object also carries `vintage`. Two synthetic rows with the same level/id and vintages `v1` and `v2` merged into one bucket; reversing input order changed which vintage was emitted. The first geography object is retained at `scripts/build_series_catalog.py:230,344,404`. This conflicts with the ADR requirement that relevant boundary vintage participate in identity (`docs/adr-arch-fact-identity-v2.md:175,350-351`). Disposition 4 therefore passes for current IDs but not for the claimed identity mechanics. + +### 3. [HIGH] Automatically generated aliases can merge distinct concepts and flip the canonical concept without curation + +The fallback treats aliases as identity-authoritative, but the catalog does not distinguish curated aliases from mechanically copied `measure.source_concept` values: + +- `source_concepts` participate in matching (`scripts/build_series_catalog.py:329`). +- A unique global name hit is accepted (`:295-300`). +- The prior concept becomes canonical (`:330-332`), and buckets landing on that key merge (`:333-368`). +- Raw concepts, source concepts, and curated aliases are all unioned into the same alias list (`:391-395`). +- `claim_uuid` runs only after this merge (`:373-390`), so it sees one bucket and cannot report that another concept was absorbed. + +Reproduction without any hand edit: + +1. Build `agency.rate_a` with source concept `OFFICIAL_SHARED`; the generator automatically records `OFFICIAL_SHARED` as an alias. +2. On the next build, supply only genuinely different `agency.rate_b` with that same source concept. The output silently remains canonical `agency.rate_a` and inherits its UUID. +3. Supply A and B together. They silently become one A row with `observation_count: 2`. + +So the answers to both adversarial questions are **yes**: a bad unique alias can merge distinct identities, and canonicalization can flip a concept without curation. This contradicts the manual-curation claim at `scripts/build_series_catalog.py:25-33` and `ledger/series_catalog.json:2`. The current tests cover a manually inserted alias with one incoming bucket (`tests/test_build_series_catalog.py:145-155`), not an automatically derived alias, two-bucket merge, or uncurated concept flip. + +The committed data demonstrate that `source_concept` is not necessarily a synonym. Raw row 104 is the derived concept `fns.snap.share_jurisdictions_at_or_above_6pct` but declares `fns.snap.total_payment_error_rate` as its source concept (`ledger/official_observations.jsonl:104`). The catalog emits the base measure as an alias on the derived share (`ledger/series_catalog.json:2813-2836`). That name is also canonical for 54 different FNS rows, yet it is absent from the six-item `ambiguous_aliases` header (`ledger/series_catalog.json:8-14`) because ambiguity counts alias occurrences only, not alias-versus-canonical collisions (`scripts/build_series_catalog.py:481-484`). + +Alias healing is also incomplete in the opposite direction. Exact identity wins before aliases are considered (`scripts/build_series_catalog.py:292-294`), so two prior rows remain separate even when one explicitly aliases the other's canonical concept. Three live same-geography/entity pairs do this: + +- initial claims: `ledger/series_catalog.json:2181-2210` versus `:5553-5584`; +- housing starts: `:1739-1769` versus `:5487-5518` (raw row 31 calls this a duplicate Thesis target ID); +- industrial production: `:2648-2678` versus `:5618-5649` (raw row 27 calls this a duplicate Thesis target ID). + +This is why the initial-claims “heal” is only partial, not complete. + +### 4. [HIGH] Logical UUID collisions pass `claim_uuid`, validation, and CI + +Both collision mechanisms key on the UUID's raw JSON string (`scripts/build_series_catalog.py:373-381,518-531`). `uuid.UUID` accepts uppercase, hyphenless, and braced representations, but the uniqueness map never keys on the parsed 128-bit value and never requires canonical `str(parsed)` spelling. + +In temporary copies, row 2 was assigned an uppercase, hyphenless, and then braced spelling of row 1's UUID. Each variant represented the same UUID after parsing; bare `--check` nevertheless printed `catalog current: 201 series` and exited 0. `tests/test_build_series_catalog.py:198-208` covers only byte-identical duplicate strings. + +The current 201 committed values are canonical lowercase UUIDv4 strings and unique by parsed value, so this is a verifier/collision-surface defect rather than a current catalog collision. + +### 5. [MEDIUM] The seed digest is load-bearing, but the default seed is not required to exist + +Positive result: the tracked seed hashes to `930424fb48c0be4c9e2ce17d4e0f2a6be886408e814c80324174a7a303fa0271`, exactly matching `ledger/series_catalog.json:6`. A one-byte seed change makes `--check` fail. The digest is therefore genuinely load-bearing (`scripts/build_series_catalog.py:414-417,500-504,565-576`). + +Residual failure: a missing path is silently treated as “no docket” (`scripts/build_series_catalog.py:414-417`), with a null digest (`:502-504`). In a temporary checkout with the seed absent, bare regeneration succeeded and wrote 155 observed/0 docket-only rows; the subsequent bare `--check` passed against that reduced catalog. The committed-catalog test does not assert seed existence, a non-null seed digest, or 201 rows (`tests/test_build_series_catalog.py:211-220`). Thus deleting/losing the seed and committing the regenerated reduced artifact reopens the original omission path while CI remains green. + +### 6. [MEDIUM] Period coverage is fixed for current data, but “only period tokens” is still not enforced + +All previously missed live spellings now normalize and the committed `suspect_segments` list is empty (`ledger/series_catalog.json:7`). BoE and M3 demonstrate useful fixes. However, stripping remains `shape OR derived` (`scripts/build_series_catalog.py:173-177`), and the shape grammar is independent of `row.period` (`:94-105`). It still strips impossible or mismatched strings such as `2026_13` or `2025_12` on a row whose period is 2026-06, with no suspect signal because suspect scanning only sees surviving segments (`:185-190`). A legitimate table, statute, cohort, or edition segment equal to a recognized period spelling is therefore silently removed. + +The claimed semantic safety pass does not mitigate current normal forms: every token derived for normal fiscal-year/month/quarter/week values is already accepted by the shape grammar. In particular, the test comment saying the grammar does not know `june_2026` (`tests/test_build_series_catalog.py:63-69`) is false; the month regex already recognizes it (`scripts/build_series_catalog.py:99`). + +This is an acceptable residual only if date-shaped dotted segments are explicitly reserved for periods by contract. Under the present categorical “period tokens (and nothing else)” statement (`scripts/build_series_catalog.py:11-17`), it is not acceptable: a mismatch should at least fail/flag, or the format needs an escape/curation mechanism. + +## Catalog audit + +The committed artifact contains 201 rows: 155 observed and 46 docket-only. All 168 observations are accounted for in observed-row counts. The 75 seed entries yield 46 docket-only rows; 29 match observed names. All 46 direct seed rows match their declared cadence, target unit, and country mapping; entries without a country retain null geography. Both input digests are exact, and all current UUIDs are parseable canonical UUIDv4 values unique by parsed value. + +### Twelve-row raw-data spot-check + +| Catalog identity | Raw/seed evidence | Result | +|---|---|---| +| BoE Bank Rate (`ledger/series_catalog.json:1619-1650`) | raw `ledger/official_observations.jsonl:39-40` | Correct GB/government/bank-rate identity; both spellings merge, count 2. | +| Census M3 orders (`ledger/series_catalog.json:1805-1835`) | raw `:157`; seed `ledger/seeds/thesis_docket_series.json:546-568` | Correct US/economy identity; dated concept and docket seed heal to one observed row. | +| Census M3 shipments (`ledger/series_catalog.json:1838-1868`) | raw `:158`; seed `:571-592` | Correct US/economy identity; one observed row. | +| FNS national (`ledger/series_catalog.json:2845-2873`) | raw `ledger/official_observations.jsonl:6,103` | Correct US-country/household identity; FY2024+FY2025, count 2. | +| FNS California (`ledger/series_catalog.json:2995-3024`) | raw `:54` | Exact state ID/name and household entity. | +| FNS District of Columbia (`ledger/series_catalog.json:3115-3144`) | raw `:58` | Exact state-level DC ID/name and household entity. | +| FNS Guam (`ledger/series_catalog.json:4405-4434`) | raw `:61` | Faithfully retains the raw state-level Guam ID/entity. | +| Eurostat May final (`ledger/series_catalog.json:2312-2341`) | raw `:33` | Correct EA/household/HICP-all-items identity. | +| Eurostat June flash (`ledger/series_catalog.json:2344-2374`) | raw `:134` | Correct EA21/economy identity; no longer collapsed with May final. | +| Old DOL initial claims (`ledger/series_catalog.json:2181-2210`) | raw `:18` | Faithful US/ui_initial_claimant/month metadata. | +| Standard weekly initial claims (`ledger/series_catalog.json:5520-5551`) | raw `:105-106,144,148,162` | Correct US/ui_claimant/week-ending identity, count 5. | +| Second old initial-claims spelling (`ledger/series_catalog.json:5552-5584`) | raw `:44` | Period token strips, but row remains separate despite aliasing the first old-DOL concept. | + +FNS now splits correctly: 55 observations become 54 geographic identities — one national identity with two periods plus 53 state-level jurisdiction identities. The raw and catalog geography/entity sets agree exactly. + +The initial-claims three-row split reflects inconsistent raw metadata rather than three cleanly distinct economic concepts: + +1. raw row 18: `dol.eta...`, entity role `ui_initial_claimant`, period type `month`; +2. raw row 44: `us.dol...`, the same `ui_initial_claimant`/`month` dimensions, and an alias back to the first concept; +3. raw rows 105-106, 144, 148, 162: `us.dol...`, role `ui_claimant`, proper `week_ending` cadence. + +The year-bearing token defect is fixed, and future observations matching each normalized bucket will not mint a UUID per week. But the existing semantic duplication is not healed: rows 1 and 2 still have separate UUIDs even though one aliases the other's canonical concept. + +## Six prior dispositions + +| Prior issue | Re-review result | +|---|---| +| 1. Untracked seed / bare check | **Partial pass.** Seed is tracked, hashed, and bare check uses it; a byte change fails. Missing seed plus regenerated reduced catalog still passes. | +| 2. Period spellings | **Qualified pass.** All live spellings normalize; BoE and M3 heal and suspects are zero. Initial claims remains three rows, and false-positive stripping is still possible/unflagged. | +| 3. Rename/curation/collision | **Fail.** The commits remint every UUID; auto aliases can merge/flip concepts; geography can move; parsed-equivalent UUID collisions pass; current alias-linked duplicates persist. | +| 4. Concept-only geography collapse | **Partial pass.** Current FNS and Eurostat rows are correctly split, but fallback ignores dimensions and geography vintage is absent from the key. | +| 5. Modal unit/cadence | **Pass.** `_modal` is gone and synthetic observation unit and cadence conflicts both hard-fail through `_sole` (`scripts/build_series_catalog.py:257-266,402-403`). | +| 6. Provenance / no fabricated default geography | **Pass narrowly.** Both digests match exact bytes; seed-only undeclared countries remain null; UUID state is correctly described as catalog state rather than input derivation. | + +## CI and validation + +The CI step is wired correctly and unconditional for pushes and pull requests: it runs bare `--check` and the focused tests with no error suppression (`.github/workflows/ci.yml:37-40`). An ordinary stale derived field, one-byte seed change, observation change, or missing seed against the current 201-row catalog exits 1 and fails the step. + +Its boundary is material: because the catalog under check supplies UUID/alias/canonical state, valid UUID remints and persisted alias changes are considered current. Both the `6ab4fbe` and HEAD UUID-disjoint catalogs pass the HEAD checker. Missing seed plus a consistently regenerated 155-row catalog also passes. Therefore CI is a derived-data freshness check, not an identity-continuity or required-seed check. + +Validation record: + +```text +python3 scripts/build_series_catalog.py --check PASS (201) +pytest tests/test_build_series_catalog.py -q PASS (34) +python3 -m doctest scripts/build_series_catalog.py PASS +ruff check script + focused tests PASS +git diff --check 9b4329e..HEAD PASS +one-byte seed change vs current catalog FAIL as stale (correct) +missing seed vs current catalog FAIL as stale (correct) +missing seed + regenerated 155-row catalog INCORRECT PASS +uppercase/hyphenless/braced duplicate UUID INCORRECT PASS +HEAD checker against 6ab4fbe UUID catalog INCORRECT PASS (201) +current FNS split PASS (54 identities / 55 observations) +current UUID syntax/version/parsed uniqueness PASS (201) +final worktree CLEAN +``` + +## Merge gate + +Do not merge until, at minimum: + +1. The surviving UUID for every pre-existing/merged identity is explicitly curated and the catalog is rebuilt without wholesale reminting; continuity must be checked against the prior committed registry, not only against itself. +2. Alias fallback is scoped to compatible identity dimensions (with an explicit docket-placeholder enrichment rule), and derived source concepts are not treated as curated synonyms without provenance or review. +3. UUIDs are required to use canonical text and uniqueness is keyed by parsed UUID value. +4. Geography boundary vintage participates in identity and required seed absence is a hard error. +5. Period normalization either reserves date-shaped segments by contract or flags mismatches/false-positive candidates. + +**Final recommendation: BLOCK.** + diff --git a/review-context-v3-disposition.md b/review-context-v3-disposition.md new file mode 100644 index 0000000..58e8f78 --- /dev/null +++ b/review-context-v3-disposition.md @@ -0,0 +1,79 @@ +# Disposition — series-catalog v3 (`3cb0c8a`) + +Response to the second adversarial review (BLOCK). Core change: UUID authority moves out of the catalog file into **`ledger/series_uuid_registry.jsonl`, an append-only minting ledger** (one JSON line per identity→UUID binding; re-bindings must chain via `supersedes` + `note`). The catalog is now a derived view that embeds the registry digest. The circular-state defect — "the file under check first supplies the prior UUIDs, and the generated result is then compared with that same file" — is gone: the builder inherits from the registry (`scripts/build_series_catalog.py:840-866`), and `--check` verifies catalog↔registry agreement binding-for-binding (`:1063-1083`). + +## Finding-by-finding + +**1. [CRITICAL] Wholesale remint undetectable by `--check` — fixed, and the committed UUIDs are frozen.** +The registry was bootstrapped from the `2859ecb` catalog's 201 bindings (the prior committed registry state); v3 regeneration preserved **201/201 UUIDs** (identity-key → UUID map verified equal). Your exact repro now fails: swapping the `6ab4fbe` catalog in and running the HEAD checker exits 1 with per-identity `registry agreement` errors (one per reminted UUID). Layers, all exercised by tests: +- catalog row ≠ registry binding → `--check` fails (`registry_agreement_problems`, test `test_check_rejects_uuid_disjoint_catalog`); +- registry edited in a working tree → `--check` fails the git-HEAD byte-prefix check (`:1113-1125`); +- registry edited across commits → the new CI step fails the PR (`.github/workflows/ci.yml:41-63`, `--verify-registry-append-only` against `github.event.pull_request.base.sha`); +- write mode refuses to change or drop any existing identity's UUID absent `--allow-remint --remint-note "..."`; a permitted remint appends a chained supersede line, so every identity change is a reviewable event (`:1245-1263`, tests `test_main_remint_guard_and_ceremony`, `test_main_dropped_identity_requires_allow_remint`). + +**2. [HIGH] Alias fallback bypasses geography/entity; vintage missing from key — fixed.** +`ExistingCatalog.match` searches only rows with the same `(geography, entity)` key (`:554-609`); your cross-geography repro now mints fresh (test `test_cross_geography_name_match_never_inherits`), and incremental geography addition no longer trips the global-ambiguity error (test `test_new_geography_added_incrementally`). The documented exception is docket-placeholder enrichment: docket-only row, entity `None`, geography absent or equal on `(level, id)` (tests `test_docket_placeholder_enrichment_keeps_uuid`, `test_docket_placeholder_never_enriches_across_country`). Geography **vintage** joins the identity key (`_geo_key`, `:421-423`) and the registry stores it per binding; same level/id with different vintages now yields two identities, order-independent (test `test_geography_vintage_splits_identity`). All 168 live observations carry `vintage: "current"`, so no live identity moved. + +**3. [HIGH] Auto source-concept aliases merge/flip concepts — fixed.** +`measure.source_concept` values are provenance now: recorded per row in a new `source_concepts` field, excluded from `aliases`, excluded from match names (`:817`, `:894`). Your `OFFICIAL_SHARED` repro: rate_b mints fresh, and A+B together stay two rows with no canonical flip (test `test_source_concept_never_drives_inheritance`). Row 104's derived share no longer aliases `fns.snap.total_payment_error_rate` — it cites it as `source_concepts` provenance. The one-time cleanup of the 85 machine aliases was done as reviewed data curation (not version-gated code); the two docket links that genuinely were identity statements — `bls.ces.average_hourly_earnings_private`, `statcan.employment_insurance.regular_beneficiaries` — were re-added as explicit curated aliases. `ambiguous_aliases` is now empty. The three alias-linked live pairs (initial claims, housing starts, industrial production) **deliberately remain separate rows**: with source labels demoted to provenance, no alias relation links them any more; folding each pair is a curation judgment (delete absorbed row + curated alias + `--allow-remint`, per the module docstring recipe) that should be its own reviewed change, not a mechanical side effect of this one. + +**4. [HIGH] Logical UUID collisions pass — fixed.** +Canonical lowercase form is required everywhere (`canonical_uuid_problem`, `:447-460`) and uniqueness keys on the parsed 128-bit value (`validate_uuids`, `:1035-1060`; `claim_uuid`, `:824-838`; registry load). Your uppercase/hyphenless/braced variants each produce two findings — non-canonical form and same-128-bit-value duplicate (test `test_uuid_validation_requires_canonical_and_parsed_uniqueness`). + +**5. [MEDIUM] Missing seed silently accepted — fixed.** +A missing seed path is a hard error in write and check modes alike (`:1190-1195`, test `test_main_missing_seed_is_a_hard_error`), and the committed-catalog test additionally pins `docket_seed_sha256 is not None`, seed existence, and the 201-row count. + +**6. [MEDIUM] Shape-pass strips mismatched/impossible tokens — fixed.** +Stripping is no longer "shape OR derived": a segment strips only when it is a direct spelling of the row's own period or parses to a calendar window **overlapping** that period (`family_pattern` + `_matches_period`, `:337-398`) — this covers all eight live shape-only strips (day-in-month, week-overlapping-month, `after_`-qualified month, month-range covering the period month; fiscal-year tokens must match the fiscal-year period exactly). `2025_12` on a 2026-06 row and impossible tokens like `2026_13` are **kept in the identity and flagged** in `suspect_segments` (tests `test_mismatched_tokens_flagged_not_stripped`, doctests). The false test comment about `june_2026` is gone. Bonus: fixed a latent v2 bug where `MONTHS_ABBREV` held 13 entries, silently shifting derived abbreviations for October–December rows. + +## Regenerated artifact (all v2 heals retained) + +201 series — 155 observed, 46 docket-only; FNS 54-way split (national row with FY2024+FY2025, 53 state rows); BoE one row, count 2; both M3 rows observed; Eurostat May-final (EA/household) and June-flash (EA21/economy) separate; `suspect_segments: []`; `ambiguous_aliases: []`; `minted=0, superseded=0` on the migration build. + +## Validation record + +```text +uv run pytest tests/test_build_series_catalog.py -q 63 passed +uv run ruff check script + tests clean +python3 -m doctest scripts/build_series_catalog.py clean +python3 scripts/build_series_catalog.py --check PASS (201) +identity continuity 2859ecb -> 3cb0c8a 201/201 UUIDs preserved +HEAD checker vs 6ab4fbe catalog copy FAILS (registry agreement, exit 1) +missing seed (write / check) FAILS (hard error, both) +uppercase / hyphenless / braced duplicate uuid FAILS validation +registry line edited FAILS --check + --verify-registry-append-only +catalog uuid edited, no flag build REFUSES, nothing written +--allow-remint without --remint-note REFUSES +--allow-remint + note writes chained supersede line; --check green after +seed entry dropped, no flag build REFUSES +byte idempotence (catalog + registry) PASS +``` + +Registry bootstrap is reproducible: one mint line per `2859ecb` catalog row, in row order (script preserved at `~/thesis-wave-0731/catalog-v3-migration.py`; the agreement check makes the equivalence machine-verifiable). + +A third adversarial review is being dispatched against `3cb0c8a`. + +--- + +## Addendum (45fcc10, self-found before the third review) + +Rebuilding from a bare registry (committed catalog deleted/empty) loses +curated naming/alias memory; after any future rename curation the renamed +identity would re-key away from its binding and silently fresh-mint, +passing agreement and append-only checks. The builder now refuses a +bare-registry rebuild absent --allow-remint --remint-note +(test_rebuild_without_prior_catalog_is_gated; suite is 64 tests). + +--- + +## Addendum 2 (2c31ae4 — four findings from your predecessor's crashed run, all fixed) + +Attempt 1 of the third review crashed before its REPORT after finding: +(1) lossy pipe-joined dimension keys (now JSON-encoded, injective); +(2) partial-catalog deletion silently re-minting (registry now tracks +liveness with retired/revived events; live-binding uuids must appear in +the catalog — both directions checked); (3) silent overlap strips (new +overlap_stripped_segments audit header, currently the 8 live segments); +(4) CI append-only gate PR-only (now also push events via +github.event.before, loud on unfetchable base). Also: the migration +removed 82 machine alias instances (disposition said 85 — prose error). diff --git a/scripts/build_series_catalog.py b/scripts/build_series_catalog.py index 7b60beb..06be639 100644 --- a/scripts/build_series_catalog.py +++ b/scripts/build_series_catalog.py @@ -23,9 +23,13 @@ * in CI, ``--verify-registry-append-only BASE_FILE`` proves the PR keeps the base branch's registry as an exact byte prefix; * an identity may change or lose its UUID only through an explicit - ``--allow-remint --remint-note "..."`` run, which appends a supersede line - (``supersedes`` = the replaced UUID, chained per identity) so every remint - is a reviewable event, never a silent regeneration. + ``--allow-remint --remint-note "..."`` run, which appends chained + supersede/retire lines so every identity change is a reviewable event, + never a silent regeneration. UUIDs are never reused by ordinary mints; + the one sanctioned reuse is a retire + ``succeeds`` pair (placeholder + enrichment). Catalog rows and live bindings are kept in bijection — + same identity, same UUID, both directions — so re-keying or swapping + identities cannot pass ``--check``. Wholesale remints therefore fail ``--check`` twice over: the reminted catalog disagrees with the committed registry, and any registry rewrite @@ -40,7 +44,13 @@ ``after_mpc_june_2026``) that overlaps the row's period. Date-shaped segments that do NOT match the row's period — a disjoint window, or an impossible token like ``2026_13`` — are never stripped: they stay in the -identity and are reported in ``suspect_segments`` for curation. +identity and are reported in ``suspect_segments`` for curation. No strip +is invisible either way: every distinct stripped spelling is published in +``stripped_segments``, because a statute, cohort, or edition label that +happens to spell the row's own period is mechanically indistinguishable +from a period label — the audit list is where a curator catches that. A +malformed period (month 13, an impossible week date) is a hard error, so +corrupt metadata can never manufacture strippable tokens. Aliases are curated identity statements, not derived data. Observed concept spellings of one identity become aliases automatically; everything else in @@ -57,15 +67,17 @@ match can heal a rename within one dimension slice but can never move a UUID across geographies or entities. The one documented exception is docket-placeholder enrichment: a docket-only row (no observations; entity -unknown; geography absent or matching on level/id) may be claimed by the -first observed rows of that series, which upgrade it in place, keep its -UUID, and register the enriched identity. +unknown; geography absent or matching on level/id, with no contradicting +declared vintage) may be claimed by the first observed rows of that +series, which upgrade it in place, keep its UUID, and record the move as +a retire + ``succeeds`` event pair. Curated merges (two committed rows that are the same series) are performed by hand: delete the absorbed row, add its concept to the survivor's ``aliases``, regenerate with ``--allow-remint --remint-note``; the absorbed identity's observations inherit the survivor's UUID and the registry gains -the supersede line. +a retire line for the absorbed binding (its old UUID goes dormant with +it). Inputs are pinned: the observation JSONL (``observations_sha256``), the committed docket seed at ``ledger/seeds/thesis_docket_series.json`` @@ -267,13 +279,18 @@ def period_token_variants(period: dict) -> set[str]: m = re.fullmatch(r"(\d{4})-(\d{2})", value) if m: year, month = m.group(1), int(m.group(2)) + if not 1 <= month <= 12: + return tokens tokens.update({f"{year}-{month:02d}", f"{year}_{month:02d}"}) tokens.add(f"{MONTHS_FULL[month - 1]}_{year}") tokens.add(f"{MONTHS_ABBREV[month - 1]}_{year}") elif ptype == "quarter": m = re.fullmatch(r"(\d{4})-(\d{2})", value) if m: - year, quarter = m.group(1), (int(m.group(2)) - 1) // 3 + 1 + year, month = m.group(1), int(m.group(2)) + if not 1 <= month <= 12: + return tokens + quarter = (month - 1) // 3 + 1 tokens.update({f"q{quarter}_{year}", f"{year}_q{quarter}"}) elif ptype == "week_ending": iso = value @@ -506,6 +523,15 @@ def build_identities(rows: list[dict]) -> dict[tuple[str, str, str], dict]: f"measure.concept: {json.dumps(row)[:200]}" ) period = row.get("period") or {} + if ( + period.get("value") is not None + and period_descriptor(period) is None + ): + raise SystemExit( + f"observation row {index} has a malformed period " + f"{json.dumps(period)} — refusing to derive period tokens " + "from it (fix the period type/value upstream)" + ) pattern = family_pattern(concept_raw, period) geography = row.get("geography") or None entity = row.get("entity") or None @@ -518,7 +544,7 @@ def build_identities(rows: list[dict]) -> dict[tuple[str, str, str], dict]: "source_concepts": set(), "rid_patterns": set(), "suspects": set(), - "overlap_strips": set(), + "stripped": set(), "units": Counter(), "period_types": Counter(), "geography": geography, @@ -538,10 +564,10 @@ def build_identities(rows: list[dict]) -> dict[tuple[str, str, str], dict]: ident["suspects"].update(suspect_segments(pattern)) ident["suspects"].update(suspect_segments(rid_pattern)) for identifier in (concept_raw, rid): - ident["overlap_strips"].update( + ident["stripped"].update( segment for segment, kind in classify_segments(identifier, period) - if kind == "overlap" + if kind in ("derived", "overlap") ) ident["units"][measure.get("unit")] += 1 ident["period_types"][period.get("type")] += 1 @@ -637,6 +663,14 @@ def match( incoming.get("id"), ): continue + # A placeholder that DECLARES a boundary vintage only + # enriches observations of that vintage; an undeclared + # vintage (the docket mapping never sets one) is open. + cand_vintage = cand_geo.get("vintage") + if cand_vintage is not None and cand_vintage != ( + incoming.get("vintage") + ): + continue placeholder_hits[candidate["uuid"]] = candidate if len(placeholder_hits) > 1: raise SystemExit( @@ -649,30 +683,51 @@ def match( return None +def _reject_duplicate_keys(pairs: list[tuple[str, object]]) -> dict: + """JSON object hook that refuses duplicate member names. + + Ordinary ``json.loads`` keeps the last value, so a line with two + ``uuid`` members means different things to different parsers. + """ + obj: dict = {} + for key, value in pairs: + if key in obj: + raise ValueError(f"duplicate JSON member {key!r}") + obj[key] = value + return obj + + +def _usable_note(note: object) -> bool: + """A note must carry visible content, not just zero-width padding.""" + return isinstance(note, str) and any(c.isalnum() for c in note) + + class UuidRegistry: """The append-only UUID minting ledger. One JSON object per line, binding one identity (concept, geography - level/id/vintage, entity name/role) to a UUID. Four event kinds, chained + level/id/vintage, entity name/role) to a UUID. Five event kinds, chained per identity and never edited or removed (``--verify-registry-append- only`` and the git-HEAD prefix check enforce growth-only): - * mint — the identity's first line; no markers. - * supersede — ``supersedes`` names the previous UUID; ``note`` required. - The binding changes UUID (remint or curated merge). + * mint — the identity's first line; no markers. Its UUID must be new to + the registry: ordinary mints can never reuse another binding's UUID. + * succeeds-mint — a mint carrying ``succeeds`` (the predecessor's + identity fields). The one sanctioned form of UUID reuse: the named + predecessor must already be retired holding exactly that UUID (a + docket placeholder enriched into its observed identity). + * supersede — ``supersedes`` names the previous UUID; ``note`` required; + the new UUID must differ and be new to the registry. * retire — ``retired: true`` with the unchanged UUID; ``note`` required. The identity left the catalog; its binding stays reserved but dormant. * revive — ``revived: true`` with the unchanged UUID; written automatically when a retired identity is observed again. - A LIVE binding (latest event not a retire) must always be represented in - the catalog by its UUID — that is the liveness invariant ``--check`` - enforces, and it is what makes a silent partial rebuild impossible: any - catalog state that loses a live binding's UUID needs an explicit retire - or supersede event to become checkable again. Multiple identities may - share a UUID (a curated merge moves an identity onto the survivor's - UUID; an enriched docket placeholder registers its observed identity - beside the seed one) — the catalog still enforces one ROW per UUID. + Invariants ``--check`` builds on: live bindings and catalog rows are in + BIJECTION (same identity, same UUID, both directions), and live + bindings' UUIDs are unique by parsed value. Any state that re-keys, + swaps, or shares UUIDs without the explicit events above fails + validation or agreement. """ def __init__(self, path: pathlib.Path, raw: bytes) -> None: @@ -680,15 +735,21 @@ def __init__(self, path: pathlib.Path, raw: bytes) -> None: self.raw = raw self.entries: list[dict] = [] self.latest: dict[tuple[str, str, str], dict] = {} + # First owner of each 128-bit value, for the no-reuse rule. + self.uuid_owner: dict[int, tuple[str, str, str]] = {} problems: list[str] = [] + if b"\r" in raw: + problems.append("registry must be LF-only (CR byte found)") + if raw and not raw.endswith(b"\n"): + problems.append("registry must end with a newline") for lineno, line in enumerate(raw.decode("utf-8").splitlines(), start=1): if not line.strip(): problems.append(f"line {lineno}: blank line") continue try: - entry = json.loads(line) - except json.JSONDecodeError as exc: - problems.append(f"line {lineno}: not JSON ({exc})") + entry = json.loads(line, object_pairs_hook=_reject_duplicate_keys) + except (json.JSONDecodeError, ValueError) as exc: + problems.append(f"line {lineno}: not strict JSON ({exc})") continue if not isinstance(entry, dict) or not isinstance( entry.get("concept"), str @@ -700,28 +761,43 @@ def __init__(self, path: pathlib.Path, raw: bytes) -> None: problems.append(f"line {lineno}: {problem}") continue key = self.entry_key(entry) + parsed = uuid_module.UUID(entry["uuid"]).int previous = self.latest.get(key) supersedes = entry.get("supersedes") retired = entry.get("retired") revived = entry.get("revived") + succeeds = entry.get("succeeds") markers = sum( - 1 for marker in (supersedes, retired, revived) + 1 for marker in (supersedes, retired, revived, succeeds) if marker is not None ) - noted = isinstance(entry.get("note"), str) and entry["note"].strip() if markers > 1: problems.append( - f"line {lineno}: {key} mixes supersede/retire/revive " - "markers" + f"line {lineno}: {key} mixes " + "supersede/retire/revive/succeeds markers" ) - elif previous is None: - if markers: + elif previous is None and supersedes is None and retired is None \ + and revived is None: + owner = self.uuid_owner.get(parsed) + if succeeds is not None: + succeeds_problem = self._succeeds_problem( + lineno, key, entry, succeeds + ) + if succeeds_problem: + problems.append(succeeds_problem) + elif owner is not None: problems.append( - f"line {lineno}: {key} has no prior binding to " - "supersede/retire/revive" + f"line {lineno}: mint for {key} reuses uuid " + f"{entry['uuid']} already bound to {owner} — UUID " + "reuse needs an explicit succeeds event" ) + elif previous is None: + problems.append( + f"line {lineno}: {key} has no prior binding to " + "supersede/retire/revive" + ) elif supersedes is not None: - if entry.get("retired") or self._is_retired(previous): + if self._is_retired(previous): problems.append( f"line {lineno}: {key} supersedes a retired binding " "(revive it first)" @@ -731,9 +807,21 @@ def __init__(self, path: pathlib.Path, raw: bytes) -> None: f"line {lineno}: {key} supersedes {supersedes} but " f"prior binding is {previous['uuid']}" ) - if not noted: + if entry["uuid"] == supersedes: + problems.append( + f"line {lineno}: supersede for {key} is a no-op " + "(uuid equals supersedes)" + ) + elif self.uuid_owner.get(parsed) not in (None, key): problems.append( - f"line {lineno}: supersede for {key} requires a note" + f"line {lineno}: supersede for {key} reuses uuid " + f"{entry['uuid']} already bound to " + f"{self.uuid_owner[parsed]}" + ) + if not _usable_note(entry.get("note")): + problems.append( + f"line {lineno}: supersede for {key} requires a " + "substantive note" ) elif retired is not None: if retired is not True: @@ -747,9 +835,10 @@ def __init__(self, path: pathlib.Path, raw: bytes) -> None: f"line {lineno}: retire for {key} must keep uuid " f"{previous['uuid']}" ) - if not noted: + if not _usable_note(entry.get("note")): problems.append( - f"line {lineno}: retire for {key} requires a note" + f"line {lineno}: retire for {key} requires a " + "substantive note" ) elif revived is not None: if revived is not True: @@ -770,12 +859,49 @@ def __init__(self, path: pathlib.Path, raw: bytes) -> None: ) self.entries.append(entry) self.latest[key] = entry + self.uuid_owner.setdefault(parsed, key) + live_by_uuid: dict[int, tuple[str, str, str]] = {} + for key, entry in self.latest.items(): + if self._is_retired(entry): + continue + parsed = uuid_module.UUID(entry["uuid"]).int + other = live_by_uuid.get(parsed) + if other is not None: + problems.append( + f"live bindings {other} and {key} share uuid " + f"{entry['uuid']} — retire or supersede one explicitly" + ) + live_by_uuid[parsed] = key if problems: raise SystemExit( "uuid registry invalid:\n" + "\n".join(f" {p}" for p in problems) ) + def _succeeds_problem( + self, lineno: int, key: tuple, entry: dict, succeeds: object + ) -> str | None: + if not isinstance(succeeds, dict): + return f"line {lineno}: succeeds must be an identity object" + predecessor_key = self.entry_key(succeeds) + predecessor = self.latest.get(predecessor_key) + if predecessor is None: + return ( + f"line {lineno}: {key} succeeds unknown identity " + f"{predecessor_key}" + ) + if not self._is_retired(predecessor): + return ( + f"line {lineno}: {key} succeeds a LIVE binding " + f"{predecessor_key} — retire it first" + ) + if predecessor["uuid"] != entry["uuid"]: + return ( + f"line {lineno}: {key} succeeds {predecessor_key} but " + f"carries uuid {entry['uuid']} != {predecessor['uuid']}" + ) + return None + @staticmethod def _is_retired(entry: dict) -> bool: return entry.get("retired") is True @@ -829,17 +955,22 @@ def render_entry(entry: dict) -> str: ordered["note"] = entry["note"] elif entry.get("revived") is not None: ordered["revived"] = True + elif entry.get("succeeds") is not None: + ordered["succeeds"] = entry["succeeds"] return json.dumps(ordered, ensure_ascii=False) - def stage(self, new_entries: list[dict]) -> None: - """Add entries to the in-memory registry (no file write yet).""" - for entry in new_entries: - self.entries.append(entry) - self.latest[self.entry_key(entry)] = entry + def stage(self, new_entries: list[dict]) -> "UuidRegistry": + """Return a new registry with entries appended and REVALIDATED. + + Revalidation reruns the entire event grammar over the staged bytes, + so no writer path can ever put an invalid chain on disk while + reporting success. + """ addition = "".join( self.render_entry(entry) + "\n" for entry in new_entries ) - self.raw = self.raw + addition.encode("utf-8") + staged_raw = self.raw + addition.encode("utf-8") + return UuidRegistry(self.path, staged_raw) def write(self) -> None: self.path.write_bytes(self.raw) @@ -887,7 +1018,6 @@ def build_catalog( # canonical concept (curation owns naming; observed spellings become # aliases). Buckets landing on the same canonical identity merge. canonical: dict[tuple[str, str, str], dict] = {} - rekeys: dict[tuple[str, str, str], tuple[str, str, str]] = {} for key in sorted(identities): ident = identities[key] concept, geo_key, entity_key = key @@ -895,8 +1025,6 @@ def build_catalog( prior = existing.match(key, names, ident["geography"], ident["entity"]) canon_concept = prior["concept"] if prior else concept canon_key = (canon_concept, geo_key, entity_key) - if canon_key != key: - rekeys[key] = canon_key bucket = canonical.setdefault( canon_key, { @@ -906,7 +1034,7 @@ def build_catalog( "source_concepts": set(), "rid_patterns": set(), "suspects": set(), - "overlap_strips": set(), + "stripped": set(), "units": Counter(), "period_types": Counter(), "geography": ident["geography"], @@ -929,7 +1057,7 @@ def build_catalog( bucket["source_concepts"] |= ident["source_concepts"] bucket["rid_patterns"] |= ident["rid_patterns"] bucket["suspects"] |= ident["suspects"] - bucket["overlap_strips"] |= ident["overlap_strips"] + bucket["stripped"] |= ident["stripped"] bucket["units"] += ident["units"] bucket["period_types"] += ident["period_types"] bucket["sources"] |= ident["sources"] @@ -939,6 +1067,7 @@ def build_catalog( series: list[dict] = [] used_uuids: dict[int, tuple] = {} plan: dict[str, list] = { + "enrich_retires": [], "mints": [], "revives": [], "supersedes": [], @@ -971,6 +1100,17 @@ def resolve_uuid( if prior_uuid and binding and prior_uuid != binding: # The catalog row disagrees with the registry: an explicit, # gated remint (the curator edited the row's uuid on purpose). + # A retired binding is revived first so the event chain stays + # valid (revives are staged before supersedes). + if not registry.is_live(canon_key): + plan["revives"].append( + dict( + _registry_event( + canon_key[0], geography, entity, binding + ), + revived=True, + ) + ) plan["supersedes"].append( _registry_event( canon_key[0], geography, entity, prior_uuid, binding @@ -992,13 +1132,63 @@ def resolve_uuid( ) return binding row_uuid = prior_uuid if prior_uuid else str(uuid_module.uuid4()) + owner_key = registry.uuid_owner.get(uuid_module.UUID(row_uuid).int) + if owner_key is not None: + prior_own_key = ( + UuidRegistry.entry_key(prior) if prior is not None else None + ) + if ( + prior is not None + and prior.get("status") == "docket-only" + and owner_key == prior_own_key + and registry.is_live(owner_key) + ): + # Docket-placeholder enrichment: the binding MOVES to the + # observed identity via an explicit retire + succeeds pair. + # UUID continuity is preserved, so no ceremony flag needed. + plan["enrich_retires"].append( + dict( + _registry_event( + prior["concept"], + prior.get("geography"), + prior.get("entity"), + row_uuid, + ), + retired=True, + note=( + "docket placeholder enriched by first observed " + "identity" + ), + ) + ) + plan["mints"].append( + dict( + _registry_event( + canon_key[0], geography, entity, row_uuid + ), + succeeds={ + "concept": prior["concept"], + "geography": _identity_geography( + prior.get("geography") + ), + "entity": _identity_entity(prior.get("entity")), + }, + ) + ) + return row_uuid + raise SystemExit( + f"identity {canon_key} would mint uuid {row_uuid}, which " + f"the registry already binds to {owner_key} — UUID reuse " + "requires an explicit ceremony (merge via curated alias + " + "--allow-remint, or placeholder enrichment)" + ) plan["mints"].append( _registry_event(canon_key[0], geography, entity, row_uuid) ) return row_uuid all_suspects: set[str] = set() - all_overlap_strips: set[str] = set() + all_stripped: set[str] = set() for canon_key in sorted(canonical): bucket = canonical[canon_key] concept, _, _ = canon_key @@ -1010,7 +1200,7 @@ def resolve_uuid( curated_aliases = set(prior.get("aliases", [])) if prior else set() aliases = sorted((bucket["concepts"] | curated_aliases) - {concept}) all_suspects.update(bucket["suspects"]) - all_overlap_strips.update(bucket["overlap_strips"]) + all_stripped.update(bucket["stripped"]) series.append({ "uuid": row_uuid, "concept": concept, @@ -1034,22 +1224,27 @@ def resolve_uuid( docket_raw = docket_path.read_bytes() docket = json.loads(docket_raw.decode()) alias_tally = Counter(alias for row in series for alias in row["aliases"]) - claimed_names: set[str] = set() + name_rows: dict[str, list[dict]] = {} for row in series: - claimed_names.add(row["concept"]) + name_rows.setdefault(row["concept"], []).append(row) for alias in row["aliases"]: if alias_tally[alias] == 1: - claimed_names.add(alias) + name_rows.setdefault(alias, []).append(row) + seen_docket_names: set[str] = set() for entry in docket["series"]: concept = entry["series"] + if concept in seen_docket_names: + raise SystemExit( + f"duplicate docket series id {concept!r} — docket names " + "must be unique or claiming becomes order-dependent" + ) + seen_docket_names.add(concept) cadence_word = entry.get("cadence") if cadence_word not in CADENCE_TO_PERIOD_TYPE: raise SystemExit( f"docket cadence {cadence_word!r} for {concept} has no " "period-type mapping; extend CADENCE_TO_PERIOD_TYPE" ) - if concept in claimed_names: - continue extras = entry.get("extras") or {} country = extras.get("country") geography = None @@ -1060,13 +1255,28 @@ def resolve_uuid( f"docket entry {concept} has country {country!r} with " "no geography mapping; extend COUNTRY_GEOGRAPHY" ) + # A name claim only counts within the entry's declared + # dimension: a GB observation must not swallow a US docket + # entry of the same name. + claimants = name_rows.get(concept, []) + if geography is not None: + claimants = [ + row + for row in claimants + if ( + (row.get("geography") or {}).get("level"), + (row.get("geography") or {}).get("id"), + ) == (geography["level"], geography["id"]) + ] + if claimants: + continue key = (concept, _geo_key(geography), _entity_key(None)) prior = existing.match(key, {concept}, geography, None) row_uuid = claim_uuid( resolve_uuid(key, prior, geography, None), key ) curated_aliases = set(prior.get("aliases", [])) if prior else set() - series.append({ + docket_row = { "uuid": row_uuid, "concept": concept, "family_patterns": [family_pattern(concept)], @@ -1082,8 +1292,9 @@ def resolve_uuid( "first_observed_period": None, "last_observed_period": None, "observation_count": 0, - }) - claimed_names.add(concept) + } + series.append(docket_row) + name_rows.setdefault(concept, []).append(docket_row) series.sort(key=lambda row: ( row["concept"], @@ -1091,42 +1302,28 @@ def resolve_uuid( _entity_key(row.get("entity")), )) - # Absorbed identities: an observed bucket that canonicalized onto a - # different identity moves that identity's registry binding onto the - # surviving UUID (a supersede event) if it pointed elsewhere. + # Liveness is IDENTITY-AWARE: every live registry binding must have a + # catalog row at exactly its identity carrying exactly its UUID. A + # binding losing that (row deleted, identity re-keyed, observations + # absorbed by a curated merge) needs an explicit retire — the gap that + # let re-keyed identities swap or shed UUIDs while their old values + # lingered elsewhere in the catalog is closed by matching on the pair, + # never on bare UUID membership. row_uuid_by_key = { (r["concept"], _geo_key(r.get("geography")), _entity_key(r.get("entity"))): r["uuid"] for r in series } - for original_key, canon_key in sorted(rekeys.items()): - old_binding = registry.binding(original_key) - surviving = row_uuid_by_key.get(canon_key) - if old_binding and surviving and old_binding != surviving: - ident = identities[original_key] - plan["supersedes"].append( - _registry_event( - original_key[0], - ident["geography"], - ident["entity"], - surviving, - old_binding, - ) - ) - - # Liveness: every live registry binding must keep its UUID somewhere in - # the catalog. A binding that loses it needs an explicit retire (or is - # covered by a planned supersede). This is what makes a partial rebuild - # against a truncated catalog loud instead of silently re-minting. new_uuids = {r["uuid"] for r in series} - superseding_keys = { - UuidRegistry.entry_key(event) for event in plan["supersedes"] + planned_keys = { + UuidRegistry.entry_key(event) + for event in plan["supersedes"] + plan["enrich_retires"] } retire_keys: set[tuple[str, str, str]] = set() for key, entry in registry.live_bindings(): - if entry["uuid"] in new_uuids: + if row_uuid_by_key.get(key) == entry["uuid"]: continue - if key in superseding_keys: + if key in planned_keys: continue plan["retire_pending"].append( dict( @@ -1152,7 +1349,9 @@ def resolve_uuid( continue plan["dropped"].append((prior_key, prior_row["uuid"])) - for kind in ("mints", "revives", "supersedes", "retire_pending"): + for kind in ( + "enrich_retires", "mints", "revives", "supersedes", "retire_pending", + ): plan[kind].sort( key=lambda e: (e["concept"], _geo_key(e["geography"]), _entity_key(e["entity"])) @@ -1167,8 +1366,9 @@ def resolve_uuid( "level/id/vintage, entity) identity. UUID authority is the " "append-only ledger/series_uuid_registry.jsonl (digest below): " "a uuid is minted once, inherited from the registry on every " - "regeneration, and changes only through an explicit " - "--allow-remint supersede event recorded there. Consumers " + "regeneration, and changes only through explicit, chained " + "supersede/retire/revive/succeeds events recorded there " + "(live bindings and rows stay in bijection). Consumers " "reference series by uuid or concept only. Regenerate with " "scripts/build_series_catalog.py; verify with --check. Aliases " "are curated identity statements (plus observed spellings of " @@ -1188,7 +1388,7 @@ def resolve_uuid( ), "uuid_registry_sha256": None, "suspect_segments": sorted(all_suspects), - "overlap_stripped_segments": sorted(all_overlap_strips), + "stripped_segments": sorted(all_stripped), "ambiguous_aliases": ambiguous_aliases, "series": series, } @@ -1232,20 +1432,21 @@ def registry_agreement_problems( ) -> list[str]: """Catalog/registry disagreements, in both directions. - Forward: every catalog row's identity must be bound to exactly its UUID. - Reverse (liveness): every live binding's UUID must appear on some - catalog row — a live binding whose UUID is absent means identities were - deleted without a retire/supersede event. + Catalog rows and live registry bindings must be in BIJECTION: each + row's identity is bound to exactly its UUID, and each live binding has + a catalog row at exactly its identity with exactly its UUID. Matching + on the (identity, uuid) pair — never on bare UUID membership — is what + makes re-keyed or swapped identities loud. """ problems = [] - row_uuids: set[str] = set() + row_uuid_by_key: dict[tuple[str, str, str], str] = {} for row in catalog.get("series", []): - row_uuids.add(row.get("uuid")) key = ( row["concept"], _geo_key(row.get("geography")), _entity_key(row.get("entity")), ) + row_uuid_by_key[key] = row.get("uuid") binding = registry.binding(key) if binding is None: problems.append(f"{key}: no registry binding for uuid {row['uuid']}") @@ -1254,11 +1455,16 @@ def registry_agreement_problems( f"{key}: catalog uuid {row['uuid']} != registry binding " f"{binding}" ) + elif not registry.is_live(key): + problems.append( + f"{key}: catalog row uses a RETIRED binding " + f"{row['uuid']} (regenerate to record the revive)" + ) for key, entry in registry.live_bindings(): - if entry["uuid"] not in row_uuids: + if row_uuid_by_key.get(key) != entry["uuid"]: problems.append( - f"{key}: live binding {entry['uuid']} has no catalog row — " - "retire or supersede it explicitly" + f"{key}: live binding {entry['uuid']} has no catalog row at " + "its identity — retire or supersede it explicitly" ) return problems @@ -1416,6 +1622,12 @@ def main(argv: list[str] | None = None) -> int: f"{len(plan['revives'])} retired identities observed again " "(regenerate to record the revive events)" ) + if plan["enrich_retires"]: + failures.append( + f"{len(plan['enrich_retires'])} docket placeholders enriched " + "by observations (regenerate to record the retire/succeeds " + "events)" + ) failures.extend(identity_changes) catalog["uuid_registry_sha256"] = registry.sha256() body = render(catalog) @@ -1466,8 +1678,9 @@ def main(argv: list[str] | None = None) -> int: sys.stderr.write(f"uuid validation: {problem}\n") return 1 - registry.stage( - plan["mints"] + registry = registry.stage( + plan["enrich_retires"] + + plan["mints"] + plan["revives"] + plan["supersedes"] + plan["retire_pending"] diff --git a/tests/test_build_series_catalog.py b/tests/test_build_series_catalog.py index 67a2edf..30031bf 100644 --- a/tests/test_build_series_catalog.py +++ b/tests/test_build_series_catalog.py @@ -496,7 +496,7 @@ def test_registry_chain_validation(tmp_path: pathlib.Path) -> None: path.write_text( json.dumps(mint) + "\n" + json.dumps(no_note) + "\n", encoding="utf-8" ) - with pytest.raises(SystemExit, match="requires a note"): + with pytest.raises(SystemExit, match="requires a substantive note"): bsc.UuidRegistry.load(path) @@ -748,16 +748,16 @@ def test_committed_catalog_is_current_and_valid() -> None: assert bsc.validate_uuids(committed) == [] assert bsc.registry_agreement_problems(committed, registry) == [] assert committed["suspect_segments"] == [] - # Every stripped segment that was a window-overlap judgment (rather - # than a direct spelling of the row period) stays auditable. - assert committed["overlap_stripped_segments"] == [ - "2026-06-18", - "2026_06_18", - "after_june_2026", - "after_mpc_june_2026", - "february_to_april_2026", - "week_2026-06-13", - "week_2026_06_13", + # EVERY stripped spelling is auditable — a statute or edition label + # that collides with a period spelling can only be caught here. + assert committed["stripped_segments"] == [ + "2026-05", "2026-06", "2026-06-18", "2026-07", "2026_05", + "2026_06", "2026_06_18", "2026_q2", "after_june_2026", + "after_mpc_june_2026", "april_2026", "feb_2026", + "february_to_april_2026", "fy2024", "fy2025", "june_2026", + "may_2026", "q1_2026", "week_2026-06-13", "week_2026-06-20", + "week_2026-06-27", "week_2026-07-04", "week_2026-07-11", + "week_2026-07-18", "week_2026-07-25", "week_2026_06_13", "week_ending_2026_06_06", ] assert committed["docket_seed_sha256"] is not None @@ -780,3 +780,342 @@ def test_rebuild_without_prior_catalog_is_gated( assert bsc.main( argv + ["--allow-remint", "--remint-note", "rebuild from registry"] ) == 0 + + +def _mint(concept: str, uuid: str, geography=None, entity=None) -> dict: + return { + "concept": concept, + "geography": geography, + "entity": entity, + "uuid": uuid, + } + + +U1 = "aaaaaaaa-1111-4111-8111-111111111111" +U2 = "bbbbbbbb-2222-4222-8222-222222222222" + + +def test_mint_never_reuses_a_bound_uuid(tmp_path: pathlib.Path) -> None: + # Third-review repro: re-key identities and swap their prior-catalog + # UUIDs — both "new" identities would previously mint the swapped + # values as ordinary mints. + registry_entries = [ + _mint("old.one", U1, entity={"name": "economy", "role": "aggregate"}), + _mint("old.two", U2, entity={"name": "economy", "role": "aggregate"}), + ] + existing = { + "series": [ + { + "uuid": U2, # swapped + "concept": "new.one", + "geography": dict(US), + "entity": {"name": "economy", "role": "aggregate"}, + "aliases": [], + "status": "observed", + }, + { + "uuid": U1, # swapped + "concept": "new.two", + "geography": dict(US), + "entity": {"name": "economy", "role": "aggregate"}, + "aliases": [], + "status": "observed", + }, + ] + } + with pytest.raises(SystemExit, match="already binds"): + _build( + tmp_path, + [_row("new.one"), _row("new.two")], + existing=existing, + registry_entries=registry_entries, + ) + + +def test_registry_rejects_forged_shared_uuid_mint( + tmp_path: pathlib.Path, +) -> None: + path = tmp_path / "registry.jsonl" + path.write_text( + json.dumps(_mint("a.one", U1)) + "\n" + + json.dumps(_mint("a.two", U1)) + "\n", + encoding="utf-8", + ) + with pytest.raises(SystemExit, match="reuses uuid"): + bsc.UuidRegistry.load(path) + + +def test_registry_rejects_duplicate_json_members( + tmp_path: pathlib.Path, +) -> None: + path = tmp_path / "registry.jsonl" + line = ( + '{"concept": "a.one", "geography": null, "entity": null, ' + f'"uuid": "{U1}", "uuid": "{U2}"}}' + ) + path.write_text(line + "\n", encoding="utf-8") + with pytest.raises(SystemExit, match="not strict JSON"): + bsc.UuidRegistry.load(path) + + +def test_registry_requires_lf_discipline(tmp_path: pathlib.Path) -> None: + path = tmp_path / "registry.jsonl" + body = json.dumps(_mint("a.one", U1)) + path.write_bytes(body.encode()) # no trailing newline + with pytest.raises(SystemExit, match="end with a newline"): + bsc.UuidRegistry.load(path) + path.write_bytes(body.encode() + b"\r\n") + with pytest.raises(SystemExit, match="LF-only"): + bsc.UuidRegistry.load(path) + + +def test_registry_rejects_hollow_notes_and_noop_supersedes( + tmp_path: pathlib.Path, +) -> None: + path = tmp_path / "registry.jsonl" + mint = _mint("a.one", U1) + hollow = dict(_mint("a.one", U2), supersedes=U1, note="​") + path.write_text( + json.dumps(mint) + "\n" + json.dumps(hollow) + "\n", encoding="utf-8" + ) + with pytest.raises(SystemExit, match="substantive note"): + bsc.UuidRegistry.load(path) + noop = dict(_mint("a.one", U1), supersedes=U1, note="says nothing changed") + path.write_text( + json.dumps(mint) + "\n" + json.dumps(noop) + "\n", encoding="utf-8" + ) + with pytest.raises(SystemExit, match="no-op"): + bsc.UuidRegistry.load(path) + + +def test_agreement_is_identity_aware(tmp_path: pathlib.Path) -> None: + # Rows carrying live UUIDs under the WRONG identities must fail even + # though every UUID is "present somewhere" in the catalog. + entries = [_mint("a.one", U1), _mint("a.two", U2)] + registry = _registry(tmp_path, entries) + catalog = { + "series": [ + {"uuid": U1, "concept": "b.one", "geography": None, "entity": None}, + {"uuid": U2, "concept": "b.two", "geography": None, "entity": None}, + ] + } + problems = bsc.registry_agreement_problems(catalog, registry) + assert sum("no catalog row at its identity" in p for p in problems) == 2 + + +def test_enrichment_records_retire_and_succeeds_events( + tmp_path: pathlib.Path, +) -> None: + seed = { + "series": [ + { + "series": "census.m3.new_orders", + "cadence": "monthly", + "extras": {"country": "US", "targetUnit": "percent"}, + } + ] + } + argv = _repo(tmp_path, [_row("bls.cps.unemployment_rate")], seed=seed) + assert bsc.main(argv) == 0 + catalog = json.loads((tmp_path / "catalog.json").read_text()) + placeholder_uuid = next( + r["uuid"] for r in catalog["series"] if r["status"] == "docket-only" + ) + (tmp_path / "obs.jsonl").write_text( + "".join( + json.dumps(r) + "\n" + for r in [ + _row("bls.cps.unemployment_rate"), + _row("census.m3.new_orders"), + ] + ), + encoding="utf-8", + ) + assert bsc.main(argv) == 0 # enrichment needs no ceremony flag + catalog = json.loads((tmp_path / "catalog.json").read_text()) + enriched = next( + r for r in catalog["series"] if r["concept"] == "census.m3.new_orders" + ) + assert enriched["status"] == "observed" + assert enriched["uuid"] == placeholder_uuid + lines = [ + json.loads(line) + for line in (tmp_path / "registry.jsonl").read_text().splitlines() + ] + retire = next(line for line in lines if line.get("retired")) + succeed = next(line for line in lines if line.get("succeeds")) + assert retire["uuid"] == succeed["uuid"] == placeholder_uuid + assert succeed["succeeds"]["concept"] == "census.m3.new_orders" + assert bsc.main(argv + ["--check"]) == 0 + + +def test_placeholder_with_conflicting_vintage_never_enriches( + tmp_path: pathlib.Path, +) -> None: + existing = { + "series": [ + { + "uuid": U1, + "concept": "labour.rate", + "geography": {"level": "country", "id": "0100000US", + "vintage": "2020"}, + "entity": None, + "aliases": [], + "status": "docket-only", + } + ] + } + catalog, plan = _build( + tmp_path, + [_row("labour.rate")], # arrives with vintage "current" + existing=existing, + registry_entries=[ + _mint( + "labour.rate", + U1, + geography={"level": "country", "id": "0100000US", + "vintage": "2020"}, + ) + ], + ) + observed = next(r for r in catalog["series"] if r["status"] == "observed") + assert observed["uuid"] != U1 + # The stranded placeholder binding is a gated retire, never silent. + assert [e["uuid"] for e in plan["retire_pending"]] == [U1] + + +def test_docket_claim_is_dimension_scoped(tmp_path: pathlib.Path) -> None: + gb_row = _row("boe.bank_rate", geography=dict(BRITAIN)) + us_entry = { + "series": "boe.bank_rate", + "cadence": "monthly", + "extras": {"country": "US", "targetUnit": "percent"}, + } + catalog, _ = _build(tmp_path, [gb_row], docket={"series": [us_entry]}) + assert len(catalog["series"]) == 2 # GB observed + US docket-only + statuses = { + (r["geography"] or {}).get("id"): r["status"] for r in catalog["series"] + } + assert statuses == {"GB": "observed", "0100000US": "docket-only"} + + undeclared = {"series": "boe.bank_rate", "cadence": "monthly"} + catalog, _ = _build(tmp_path, [gb_row], docket={"series": [undeclared]}) + assert len(catalog["series"]) == 1 # no country claim: GB row claims + + with pytest.raises(SystemExit, match="duplicate docket series id"): + _build( + tmp_path, + [gb_row], + docket={"series": [undeclared, dict(undeclared)]}, + ) + + +def test_statute_spelling_stripped_but_audited( + tmp_path: pathlib.Path, +) -> None: + # A statute/edition label spelling the row's own period is + # indistinguishable from a period label; it strips, but the spelling + # is published for curation rather than vanishing. + catalog, _ = _build( + tmp_path, + [_row("agency.statute.2026_05.rate", + rid="agency.statute.2026_05.rate.first_print")], + ) + assert catalog["series"][0]["concept"] == "agency.statute.rate" + assert "2026_05" in catalog["stripped_segments"] + + +@pytest.mark.parametrize( + "period", + [ + {"type": "month", "value": "2026-13"}, + {"type": "quarter", "value": "2026-13"}, + {"type": "week_ending", "value": "2026-13-40"}, + ], +) +def test_malformed_periods_are_hard_errors( + tmp_path: pathlib.Path, period: dict +) -> None: + with pytest.raises(SystemExit, match="malformed period"): + _build(tmp_path, [_row("agency.rate", period=period)]) + + +def test_remint_of_retired_identity_stages_valid_chain( + tmp_path: pathlib.Path, +) -> None: + # Third-review repro: retire an identity, then bring it back with an + # edited prior-catalog UUID under --allow-remint. The writer must + # stage revive THEN supersede (a valid chain) — and staging always + # revalidates the whole registry before anything is written. + argv = _repo(tmp_path, [_row("bls.cps.unemployment_rate")]) + assert bsc.main(argv) == 0 + (tmp_path / "seed.json").write_text( + json.dumps({"series": []}), encoding="utf-8" + ) + assert bsc.main(argv + ["--allow-remint", "--remint-note", "cut"]) == 0 + (tmp_path / "seed.json").write_text(json.dumps(SEED), encoding="utf-8") + catalog = json.loads((tmp_path / "catalog.json").read_text()) + # Hand-plant a divergent uuid for the returning docket identity. + replacement = "cccccccc-3333-4333-8333-333333333333" + lines = (tmp_path / "registry.jsonl").read_text().splitlines() + retired_uuid = json.loads(lines[-1])["uuid"] + catalog["series"].append( + { + "uuid": replacement, + "concept": "abs.labour.unemployment_rate", + "geography": bsc.COUNTRY_GEOGRAPHY["AU"], + "entity": None, + "aliases": [], + "status": "docket-only", + } + ) + (tmp_path / "catalog.json").write_text( + json.dumps(catalog, indent=2) + "\n", encoding="utf-8" + ) + assert bsc.main(argv) == 1 # gated + assert ( + bsc.main(argv + ["--allow-remint", "--remint-note", "planned swap"]) + == 0 + ) + events = [ + json.loads(line) + for line in (tmp_path / "registry.jsonl").read_text().splitlines() + ] + revive = events[-2] + supersede = events[-1] + assert revive["revived"] is True and revive["uuid"] == retired_uuid + assert supersede["supersedes"] == retired_uuid + assert supersede["uuid"] == replacement + # The staged file reloads cleanly and the catalog checks green. + bsc.UuidRegistry.load(tmp_path / "registry.jsonl") + assert bsc.main(argv + ["--check"]) == 0 + + +def test_identity_uuid_map_matches_reviewed_anchor() -> None: + """The registry's introduction commit cannot be continuity-checked by + the append-only gate (there is no prior registry to extend), so the + identity->uuid map verified by the adversarial review of PR #128 is + pinned here. Changing ANY of the 201 bindings — or adding/retiring + one — must edit this constant in the same diff, making wholesale + remints impossible to slip through as regeneration noise. + + Update the constant only alongside registry events that justify it. + """ + import hashlib + + catalog = json.loads(bsc.CATALOG.read_text(encoding="utf-8")) + lines = sorted( + json.dumps( + [ + row["uuid"], + row["concept"], + bsc._geo_key(row.get("geography")), + bsc._entity_key(row.get("entity")), + ] + ) + for row in catalog["series"] + ) + digest = hashlib.sha256("\n".join(lines).encode()).hexdigest() + assert digest == ( + "2d2552e88662eb654597999ac92f750cadbed882703324c6f9ca1f1e3ac15345" + ) From 7b6daf06848c1bacf993a038309f82e4bfab02b0 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 1 Aug 2026 15:15:56 -0400 Subject: [PATCH 08/11] Remove review briefing scratch files from the branch The review-context-*.md files are per-session adversarial-review briefing materials (prior review texts + instructions), not repo content; an over-broad git add swept them into the previous commit. Ignore the pattern so it cannot recur. Co-Authored-By: Claude Fable 5 --- .gitignore | 1 + review-context-brief.md | 97 ------------------ review-context-first-review.md | 12 --- review-context-second-review.md | 165 ------------------------------- review-context-v3-disposition.md | 79 --------------- 5 files changed, 1 insertion(+), 353 deletions(-) delete mode 100644 review-context-brief.md delete mode 100644 review-context-first-review.md delete mode 100644 review-context-second-review.md delete mode 100644 review-context-v3-disposition.md diff --git a/.gitignore b/.gitignore index 3a04aed..bab0256 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ __pycache__/ micro/us/*.parquet venv/ *.parquet +review-context-*.md diff --git a/review-context-brief.md b/review-context-brief.md deleted file mode 100644 index e888756..0000000 --- a/review-context-brief.md +++ /dev/null @@ -1,97 +0,0 @@ -# Third adversarial review brief — series-catalog v3 (commits 3cb0c8a + 45fcc10 + 2c31ae4) - -You are the third adversarial reviewer of PolicyEngine/ledger PR #128 on -branch thesis-series-catalog. The first two reviews returned BLOCK; v3 -claims to answer every finding. Your job is to try to break v3. - -Materials (untracked briefing files in this worktree root — ignore them in -any cleanliness assessment, do not commit or delete them): -- review-context-first-review.md (first BLOCK review) -- review-context-second-review.md (second BLOCK review — the v3 spec) -- review-context-v3-disposition.md (the disposition you are auditing) - -Scope: the v3 change = commits 3cb0c8a + 45fcc10 + 2c31ae4 (diff 2859ecb..2c31ae4, the branch head): -scripts/build_series_catalog.py, tests/test_build_series_catalog.py, -ledger/series_catalog.json, ledger/series_uuid_registry.jsonl (new), -.github/workflows/ci.yml. - -A previous run of this review crashed before finishing; its four findings -(lossy pipe-joined keys; partial-catalog silent re-mint; silent overlap -strips; PR-only CI append gate) are claimed FIXED in 2c31ae4 — re-verify -each fix adversarially as part of Section 2. - -RUNTIME BUDGET (hard): do NOT launch the full repository test suite (uv run -pytest with no path); it exceeds your session budget and stalled your -predecessor — CI covers it. Run the focused suite -(tests/test_build_series_catalog.py), ruff on the two changed files, -doctest, --check, and targeted experiments only. Prefer many small commands -over any long-running one; nothing you start should run longer than ~90 -seconds. - -Required work, in order: - -1. VERIFY EVERY DISPOSITION CLAIM INDEPENDENTLY. Do not trust the - disposition's validation record — rerun it: pytest, ruff, doctest, - --check, byte idempotence (catalog AND registry), the 201/201 UUID - continuity claim from 2859ecb, the 6ab4fbe catalog-swap repro, missing - seed both modes, uuid spelling variants, registry line edits vs --check - and vs --verify-registry-append-only, the remint ceremony (refuse / - note required / supersede line appended / chain validates / check green - after), the dropped-identity guard, and each healed case (FNS 54-way - incl. national count 2, BoE count 2, M3 observed, Eurostat flash/final - separation, 46 docket-only, zero suspects, empty ambiguous_aliases). - -2. ATTACK THE NEW MECHANICS. At minimum: - - Registry semantics: can you construct a registry state that validates - but lets an identity change UUIDs silently? Chain forgery? A mint - line appended for an existing identity under a cosmetically different - key spelling (geography name/vintage variations, entity null vs - missing)? Shared-uuid states that corrupt the catalog? - - Append-only enforcement: bypasses via git states (clean tree after - committing a rewritten registry — what catches it and when), the CI - step's base-sha choice, shallow clones, the file not existing at - base, trailing-newline and encoding edge cases in the byte-prefix - comparison. - - The --allow-remint ceremony: can a remint slip through without a - supersede line? Can supersede lines be written that misdescribe what - happened? Does --check really fail for every pending identity change? - - Same-dimension scoping: cross-geography/entity theft via crafted - aliases, vintage-only mismatches, null-vs-present geography, the - docket-placeholder enrichment exception (cross-country, entity - present, multiple placeholders). - - Interval-overlap stripping: tokens that overlap the row period but - are semantically NOT period labels (statute years, cohort years, - table editions shaped like dates); fiscal-year edge cases (fy token - on month rows, non-US fiscal conventions); quarter/month boundary - overlaps; impossible tokens; the suspect-flagging contract. - - Canonical UUID enforcement: any path where a non-canonical or - duplicate-by-value uuid enters the catalog or registry. - - The live artifact: spot-check rows against raw observations again - (your predecessor's 12-row table), confirm no identity moved - geography/entity/vintage vs 2859ecb, confirm the two new curated - aliases are the ONLY alias additions and are justified, confirm - source_concepts fields are faithful. - -3. JUDGE THE RESIDUALS the disposition declares deliberate: the three - alias-linked pairs left separate; dormant registry bindings after - drops; enrichment minting a second binding for the same uuid; the - one-time offline migration instead of in-code scrub. Are any of these - exploitable or dishonest rather than merely conservative? - -Rules: read-only with respect to tracked files — run all mutating -experiments on copies under /tmp, never on this worktree's tracked files; -leave `git status` clean apart from the four review-context-*.md files. -Use python3/uv, ruff, pytest as the repo does. Do not push, do not comment -on GitHub; your only output is the report. - -Output format — end your final message with exactly this structure: - -REPORT -verdict: MERGE | BLOCK -risk: LOW | MEDIUM | HIGH | CRITICAL -findings: (ranked, each with severity, file:line evidence, and a concrete -reproduction; empty section allowed only with verdict MERGE) -disposition-audit: (per disposition claim: CONFIRMED | REFUTED | PARTIAL, -one line each) -residuals-judgment: (per declared residual: ACCEPTABLE | UNACCEPTABLE + why) -validation-record: (commands you ran and their outcomes) diff --git a/review-context-first-review.md b/review-context-first-review.md deleted file mode 100644 index 7c8dcfc..0000000 --- a/review-context-first-review.md +++ /dev/null @@ -1,12 +0,0 @@ -An adversarial sol review of the first commit returned **BLOCK** with six findings; the two follow-ups respond to all of them. Review highlights and dispositions: - -1. **Untracked docket seed / bare `--check` broken** → the seed is now committed at `ledger/seeds/thesis_docket_series.json` and digest-bound in the header (`docket_seed_sha256`); bare `--check` covers the full input set and runs in CI. -2. **29 live period spellings unrecognized, already splitting UUIDs** (`2026_06`, `2026_06_18`, `feb_2026`, `week_ending_…`, `week_2026_06_13`, `after_june_2026`, `after_mpc_june_2026`, `february_to_april_2026`) → the token grammar covers every one, plus a semantic pass derives expected tokens from each row's own `period`, and any surviving year-bearing segment lands in `suspect_segments` (committed catalog: zero). The M3 and initial-claims duplicate UUIDs heal; BoE's two rate spellings merge. -3. **Rename/curation/collision loses or remints identity; `--check` trusts blindly** → UUIDs inherit by identity key, then by unique concept/alias match; curated aliases persist across regeneration and keep the prior row's canonical concept; ambiguous matches and UUID collisions are hard errors; `--check` validates UUID syntax/version/uniqueness. -4. **Concept-only key collapses 54 geography subseries (vs the fact-identity ADR)** → identity is now (concept, geography, entity): FNS error rates split into national + per-state rows; Eurostat flash (EA21/economy) and final (EA/household) separate. 141 rows → 201. -5. **Modal masking** → `_modal` is gone; unit/cadence conflicts within an identity are hard errors, never a silent pick. -6. **Provenance** → header binds observations digest + seed digest; the PR-body claim about field derivation is corrected: UUIDs are minted state preserved across regenerations, not derived from inputs; docket rows without a declared country carry null geography rather than a fabricated default. - -Regression tests (`tests/test_build_series_catalog.py`, 34 cases incl. the review's full token table) + CI step added. A focused re-review of the v2 diff runs next; merge only on green + agreement. - -🤖 Generated with [Claude Code](https://claude.com/claude-code) diff --git a/review-context-second-review.md b/review-context-second-review.md deleted file mode 100644 index 7e40493..0000000 --- a/review-context-second-review.md +++ /dev/null @@ -1,165 +0,0 @@ -# Focused re-review — series-catalog v2 - -**Reviewed range:** `9b4329e..HEAD` (`6ab4fbe`, `2859ecb`) -**Verdict:** **BLOCK** -**Risk:** **CRITICAL** — the committed artifact has discarded every previously minted UUID, the checker accepts multiple UUID-disjoint catalogs for the same inputs, and the new alias fallback can move or merge identities across the dimensions that are supposed to define them. - -The seed, current period spellings, FNS geography split, BoE merge, M3 normalization, observed-row unit/cadence rejection, and ordinary stale-catalog CI check all improved. Those passes do not offset the identity failures below. - -## Ranked findings - -### 1. [CRITICAL] The commits wholesale remint the registry, and `--check` cannot detect the remint - -The catalog promises that a UUID is “minted once and never re-minted” (`ledger/series_catalog.json:2`; also `scripts/build_series_catalog.py:5-7,41-44`). The committed history does the opposite: - -- From `9b4329e` to `6ab4fbe`, the builder's own identity projection `(concept, geography level/id, entity name/role)` has 116 common identities; **0 of 116 UUIDs survive**. The total UUID-value intersection is **0 of 141**. -- From `6ab4fbe` to `2859ecb`, all 201 identity keys are unchanged and **0 of 201 UUIDs survive**. The catalog diff consists only of 201 removed UUID lines and 201 added UUID lines; observations hash, seed hash, row counts, concepts, and metadata are unchanged. -- Example: unchanged ABS building approvals is `0a67f2eb-…` at `9b4329e:ledger/series_catalog.json:8`, `e344ff46-…` at `6ab4fbe:ledger/series_catalog.json:18`, and `efbb2901-…` now (`ledger/series_catalog.json:18`). The unchanged docket-only `abs.labour.unemployment_rate` similarly moves from `843e5bab-…` (`9b4329e:ledger/series_catalog.json:168-183`) to `443cb1bf-…` (`ledger/series_catalog.json:183-198`). -- The merged BoE row now uses `8fb890ac-…` (`ledger/series_catalog.json:1619-1650`), preserving neither prior BoE identity. M3 orders now uses `294d82e8-…` (`ledger/series_catalog.json:1805-1835`), preserving neither the prior docket nor observed UUID. - -This is not forced by the final algorithm. Running the HEAD builder against a temporary copy of the `6ab4fbe` catalog reports `catalog current: 201 series` and exits 0; running it against HEAD also exits 0. Thus two completely UUID-disjoint 201-row catalogs are accepted for the same observation and seed bytes. - -The reason is circular state: the file under check first supplies the prior UUIDs (`scripts/build_series_catalog.py:555-557`, reused at `:387-390`), and the generated result is then compared with that same file (`:565-577`). Syntax/version validation cannot establish historical continuity. This directly fails disposition 3 and means the new CI step would not have caught the actual 201-ID remint in `2859ecb`. - -### 2. [HIGH] Alias fallback bypasses geography/entity identity and omits geography vintage - -An exact identity match uses the full derived key (`scripts/build_series_catalog.py:290-294`), but the fallback searches global concept and alias indexes without filtering candidates to the incoming geography/entity (`:295-300`). The matched prior UUID is then applied to the incoming geography/entity (`:329-332,387-390`). - -Adversarial results: - -- With one prior US row and a later California-only row of the same name, the California row silently inherits the US UUID: the UUID moves to a different geography. -- If both old US and new California rows are present, `claim_uuid` eventually errors because both claim the same raw UUID. That prevents corruption but also prevents an ordinary new geography. -- Once a concept has several prior geographies (for example the 54 FNS rows), adding a new geography fails earlier as an ambiguous global name match. - -The fallback is useful for enriching a docket-only placeholder whose entity is not yet known, but it is too broad for already observed identities. There is no regression test for geography movement or incremental geography addition; `tests/test_build_series_catalog.py:119-130` only builds two geographies from an empty catalog. - -The key is also not actually the full geography object claimed by the disposition. `_geo_key` includes only `level|id` (`scripts/build_series_catalog.py:193-195`), while the observation object also carries `vintage`. Two synthetic rows with the same level/id and vintages `v1` and `v2` merged into one bucket; reversing input order changed which vintage was emitted. The first geography object is retained at `scripts/build_series_catalog.py:230,344,404`. This conflicts with the ADR requirement that relevant boundary vintage participate in identity (`docs/adr-arch-fact-identity-v2.md:175,350-351`). Disposition 4 therefore passes for current IDs but not for the claimed identity mechanics. - -### 3. [HIGH] Automatically generated aliases can merge distinct concepts and flip the canonical concept without curation - -The fallback treats aliases as identity-authoritative, but the catalog does not distinguish curated aliases from mechanically copied `measure.source_concept` values: - -- `source_concepts` participate in matching (`scripts/build_series_catalog.py:329`). -- A unique global name hit is accepted (`:295-300`). -- The prior concept becomes canonical (`:330-332`), and buckets landing on that key merge (`:333-368`). -- Raw concepts, source concepts, and curated aliases are all unioned into the same alias list (`:391-395`). -- `claim_uuid` runs only after this merge (`:373-390`), so it sees one bucket and cannot report that another concept was absorbed. - -Reproduction without any hand edit: - -1. Build `agency.rate_a` with source concept `OFFICIAL_SHARED`; the generator automatically records `OFFICIAL_SHARED` as an alias. -2. On the next build, supply only genuinely different `agency.rate_b` with that same source concept. The output silently remains canonical `agency.rate_a` and inherits its UUID. -3. Supply A and B together. They silently become one A row with `observation_count: 2`. - -So the answers to both adversarial questions are **yes**: a bad unique alias can merge distinct identities, and canonicalization can flip a concept without curation. This contradicts the manual-curation claim at `scripts/build_series_catalog.py:25-33` and `ledger/series_catalog.json:2`. The current tests cover a manually inserted alias with one incoming bucket (`tests/test_build_series_catalog.py:145-155`), not an automatically derived alias, two-bucket merge, or uncurated concept flip. - -The committed data demonstrate that `source_concept` is not necessarily a synonym. Raw row 104 is the derived concept `fns.snap.share_jurisdictions_at_or_above_6pct` but declares `fns.snap.total_payment_error_rate` as its source concept (`ledger/official_observations.jsonl:104`). The catalog emits the base measure as an alias on the derived share (`ledger/series_catalog.json:2813-2836`). That name is also canonical for 54 different FNS rows, yet it is absent from the six-item `ambiguous_aliases` header (`ledger/series_catalog.json:8-14`) because ambiguity counts alias occurrences only, not alias-versus-canonical collisions (`scripts/build_series_catalog.py:481-484`). - -Alias healing is also incomplete in the opposite direction. Exact identity wins before aliases are considered (`scripts/build_series_catalog.py:292-294`), so two prior rows remain separate even when one explicitly aliases the other's canonical concept. Three live same-geography/entity pairs do this: - -- initial claims: `ledger/series_catalog.json:2181-2210` versus `:5553-5584`; -- housing starts: `:1739-1769` versus `:5487-5518` (raw row 31 calls this a duplicate Thesis target ID); -- industrial production: `:2648-2678` versus `:5618-5649` (raw row 27 calls this a duplicate Thesis target ID). - -This is why the initial-claims “heal” is only partial, not complete. - -### 4. [HIGH] Logical UUID collisions pass `claim_uuid`, validation, and CI - -Both collision mechanisms key on the UUID's raw JSON string (`scripts/build_series_catalog.py:373-381,518-531`). `uuid.UUID` accepts uppercase, hyphenless, and braced representations, but the uniqueness map never keys on the parsed 128-bit value and never requires canonical `str(parsed)` spelling. - -In temporary copies, row 2 was assigned an uppercase, hyphenless, and then braced spelling of row 1's UUID. Each variant represented the same UUID after parsing; bare `--check` nevertheless printed `catalog current: 201 series` and exited 0. `tests/test_build_series_catalog.py:198-208` covers only byte-identical duplicate strings. - -The current 201 committed values are canonical lowercase UUIDv4 strings and unique by parsed value, so this is a verifier/collision-surface defect rather than a current catalog collision. - -### 5. [MEDIUM] The seed digest is load-bearing, but the default seed is not required to exist - -Positive result: the tracked seed hashes to `930424fb48c0be4c9e2ce17d4e0f2a6be886408e814c80324174a7a303fa0271`, exactly matching `ledger/series_catalog.json:6`. A one-byte seed change makes `--check` fail. The digest is therefore genuinely load-bearing (`scripts/build_series_catalog.py:414-417,500-504,565-576`). - -Residual failure: a missing path is silently treated as “no docket” (`scripts/build_series_catalog.py:414-417`), with a null digest (`:502-504`). In a temporary checkout with the seed absent, bare regeneration succeeded and wrote 155 observed/0 docket-only rows; the subsequent bare `--check` passed against that reduced catalog. The committed-catalog test does not assert seed existence, a non-null seed digest, or 201 rows (`tests/test_build_series_catalog.py:211-220`). Thus deleting/losing the seed and committing the regenerated reduced artifact reopens the original omission path while CI remains green. - -### 6. [MEDIUM] Period coverage is fixed for current data, but “only period tokens” is still not enforced - -All previously missed live spellings now normalize and the committed `suspect_segments` list is empty (`ledger/series_catalog.json:7`). BoE and M3 demonstrate useful fixes. However, stripping remains `shape OR derived` (`scripts/build_series_catalog.py:173-177`), and the shape grammar is independent of `row.period` (`:94-105`). It still strips impossible or mismatched strings such as `2026_13` or `2025_12` on a row whose period is 2026-06, with no suspect signal because suspect scanning only sees surviving segments (`:185-190`). A legitimate table, statute, cohort, or edition segment equal to a recognized period spelling is therefore silently removed. - -The claimed semantic safety pass does not mitigate current normal forms: every token derived for normal fiscal-year/month/quarter/week values is already accepted by the shape grammar. In particular, the test comment saying the grammar does not know `june_2026` (`tests/test_build_series_catalog.py:63-69`) is false; the month regex already recognizes it (`scripts/build_series_catalog.py:99`). - -This is an acceptable residual only if date-shaped dotted segments are explicitly reserved for periods by contract. Under the present categorical “period tokens (and nothing else)” statement (`scripts/build_series_catalog.py:11-17`), it is not acceptable: a mismatch should at least fail/flag, or the format needs an escape/curation mechanism. - -## Catalog audit - -The committed artifact contains 201 rows: 155 observed and 46 docket-only. All 168 observations are accounted for in observed-row counts. The 75 seed entries yield 46 docket-only rows; 29 match observed names. All 46 direct seed rows match their declared cadence, target unit, and country mapping; entries without a country retain null geography. Both input digests are exact, and all current UUIDs are parseable canonical UUIDv4 values unique by parsed value. - -### Twelve-row raw-data spot-check - -| Catalog identity | Raw/seed evidence | Result | -|---|---|---| -| BoE Bank Rate (`ledger/series_catalog.json:1619-1650`) | raw `ledger/official_observations.jsonl:39-40` | Correct GB/government/bank-rate identity; both spellings merge, count 2. | -| Census M3 orders (`ledger/series_catalog.json:1805-1835`) | raw `:157`; seed `ledger/seeds/thesis_docket_series.json:546-568` | Correct US/economy identity; dated concept and docket seed heal to one observed row. | -| Census M3 shipments (`ledger/series_catalog.json:1838-1868`) | raw `:158`; seed `:571-592` | Correct US/economy identity; one observed row. | -| FNS national (`ledger/series_catalog.json:2845-2873`) | raw `ledger/official_observations.jsonl:6,103` | Correct US-country/household identity; FY2024+FY2025, count 2. | -| FNS California (`ledger/series_catalog.json:2995-3024`) | raw `:54` | Exact state ID/name and household entity. | -| FNS District of Columbia (`ledger/series_catalog.json:3115-3144`) | raw `:58` | Exact state-level DC ID/name and household entity. | -| FNS Guam (`ledger/series_catalog.json:4405-4434`) | raw `:61` | Faithfully retains the raw state-level Guam ID/entity. | -| Eurostat May final (`ledger/series_catalog.json:2312-2341`) | raw `:33` | Correct EA/household/HICP-all-items identity. | -| Eurostat June flash (`ledger/series_catalog.json:2344-2374`) | raw `:134` | Correct EA21/economy identity; no longer collapsed with May final. | -| Old DOL initial claims (`ledger/series_catalog.json:2181-2210`) | raw `:18` | Faithful US/ui_initial_claimant/month metadata. | -| Standard weekly initial claims (`ledger/series_catalog.json:5520-5551`) | raw `:105-106,144,148,162` | Correct US/ui_claimant/week-ending identity, count 5. | -| Second old initial-claims spelling (`ledger/series_catalog.json:5552-5584`) | raw `:44` | Period token strips, but row remains separate despite aliasing the first old-DOL concept. | - -FNS now splits correctly: 55 observations become 54 geographic identities — one national identity with two periods plus 53 state-level jurisdiction identities. The raw and catalog geography/entity sets agree exactly. - -The initial-claims three-row split reflects inconsistent raw metadata rather than three cleanly distinct economic concepts: - -1. raw row 18: `dol.eta...`, entity role `ui_initial_claimant`, period type `month`; -2. raw row 44: `us.dol...`, the same `ui_initial_claimant`/`month` dimensions, and an alias back to the first concept; -3. raw rows 105-106, 144, 148, 162: `us.dol...`, role `ui_claimant`, proper `week_ending` cadence. - -The year-bearing token defect is fixed, and future observations matching each normalized bucket will not mint a UUID per week. But the existing semantic duplication is not healed: rows 1 and 2 still have separate UUIDs even though one aliases the other's canonical concept. - -## Six prior dispositions - -| Prior issue | Re-review result | -|---|---| -| 1. Untracked seed / bare check | **Partial pass.** Seed is tracked, hashed, and bare check uses it; a byte change fails. Missing seed plus regenerated reduced catalog still passes. | -| 2. Period spellings | **Qualified pass.** All live spellings normalize; BoE and M3 heal and suspects are zero. Initial claims remains three rows, and false-positive stripping is still possible/unflagged. | -| 3. Rename/curation/collision | **Fail.** The commits remint every UUID; auto aliases can merge/flip concepts; geography can move; parsed-equivalent UUID collisions pass; current alias-linked duplicates persist. | -| 4. Concept-only geography collapse | **Partial pass.** Current FNS and Eurostat rows are correctly split, but fallback ignores dimensions and geography vintage is absent from the key. | -| 5. Modal unit/cadence | **Pass.** `_modal` is gone and synthetic observation unit and cadence conflicts both hard-fail through `_sole` (`scripts/build_series_catalog.py:257-266,402-403`). | -| 6. Provenance / no fabricated default geography | **Pass narrowly.** Both digests match exact bytes; seed-only undeclared countries remain null; UUID state is correctly described as catalog state rather than input derivation. | - -## CI and validation - -The CI step is wired correctly and unconditional for pushes and pull requests: it runs bare `--check` and the focused tests with no error suppression (`.github/workflows/ci.yml:37-40`). An ordinary stale derived field, one-byte seed change, observation change, or missing seed against the current 201-row catalog exits 1 and fails the step. - -Its boundary is material: because the catalog under check supplies UUID/alias/canonical state, valid UUID remints and persisted alias changes are considered current. Both the `6ab4fbe` and HEAD UUID-disjoint catalogs pass the HEAD checker. Missing seed plus a consistently regenerated 155-row catalog also passes. Therefore CI is a derived-data freshness check, not an identity-continuity or required-seed check. - -Validation record: - -```text -python3 scripts/build_series_catalog.py --check PASS (201) -pytest tests/test_build_series_catalog.py -q PASS (34) -python3 -m doctest scripts/build_series_catalog.py PASS -ruff check script + focused tests PASS -git diff --check 9b4329e..HEAD PASS -one-byte seed change vs current catalog FAIL as stale (correct) -missing seed vs current catalog FAIL as stale (correct) -missing seed + regenerated 155-row catalog INCORRECT PASS -uppercase/hyphenless/braced duplicate UUID INCORRECT PASS -HEAD checker against 6ab4fbe UUID catalog INCORRECT PASS (201) -current FNS split PASS (54 identities / 55 observations) -current UUID syntax/version/parsed uniqueness PASS (201) -final worktree CLEAN -``` - -## Merge gate - -Do not merge until, at minimum: - -1. The surviving UUID for every pre-existing/merged identity is explicitly curated and the catalog is rebuilt without wholesale reminting; continuity must be checked against the prior committed registry, not only against itself. -2. Alias fallback is scoped to compatible identity dimensions (with an explicit docket-placeholder enrichment rule), and derived source concepts are not treated as curated synonyms without provenance or review. -3. UUIDs are required to use canonical text and uniqueness is keyed by parsed UUID value. -4. Geography boundary vintage participates in identity and required seed absence is a hard error. -5. Period normalization either reserves date-shaped segments by contract or flags mismatches/false-positive candidates. - -**Final recommendation: BLOCK.** - diff --git a/review-context-v3-disposition.md b/review-context-v3-disposition.md deleted file mode 100644 index 58e8f78..0000000 --- a/review-context-v3-disposition.md +++ /dev/null @@ -1,79 +0,0 @@ -# Disposition — series-catalog v3 (`3cb0c8a`) - -Response to the second adversarial review (BLOCK). Core change: UUID authority moves out of the catalog file into **`ledger/series_uuid_registry.jsonl`, an append-only minting ledger** (one JSON line per identity→UUID binding; re-bindings must chain via `supersedes` + `note`). The catalog is now a derived view that embeds the registry digest. The circular-state defect — "the file under check first supplies the prior UUIDs, and the generated result is then compared with that same file" — is gone: the builder inherits from the registry (`scripts/build_series_catalog.py:840-866`), and `--check` verifies catalog↔registry agreement binding-for-binding (`:1063-1083`). - -## Finding-by-finding - -**1. [CRITICAL] Wholesale remint undetectable by `--check` — fixed, and the committed UUIDs are frozen.** -The registry was bootstrapped from the `2859ecb` catalog's 201 bindings (the prior committed registry state); v3 regeneration preserved **201/201 UUIDs** (identity-key → UUID map verified equal). Your exact repro now fails: swapping the `6ab4fbe` catalog in and running the HEAD checker exits 1 with per-identity `registry agreement` errors (one per reminted UUID). Layers, all exercised by tests: -- catalog row ≠ registry binding → `--check` fails (`registry_agreement_problems`, test `test_check_rejects_uuid_disjoint_catalog`); -- registry edited in a working tree → `--check` fails the git-HEAD byte-prefix check (`:1113-1125`); -- registry edited across commits → the new CI step fails the PR (`.github/workflows/ci.yml:41-63`, `--verify-registry-append-only` against `github.event.pull_request.base.sha`); -- write mode refuses to change or drop any existing identity's UUID absent `--allow-remint --remint-note "..."`; a permitted remint appends a chained supersede line, so every identity change is a reviewable event (`:1245-1263`, tests `test_main_remint_guard_and_ceremony`, `test_main_dropped_identity_requires_allow_remint`). - -**2. [HIGH] Alias fallback bypasses geography/entity; vintage missing from key — fixed.** -`ExistingCatalog.match` searches only rows with the same `(geography, entity)` key (`:554-609`); your cross-geography repro now mints fresh (test `test_cross_geography_name_match_never_inherits`), and incremental geography addition no longer trips the global-ambiguity error (test `test_new_geography_added_incrementally`). The documented exception is docket-placeholder enrichment: docket-only row, entity `None`, geography absent or equal on `(level, id)` (tests `test_docket_placeholder_enrichment_keeps_uuid`, `test_docket_placeholder_never_enriches_across_country`). Geography **vintage** joins the identity key (`_geo_key`, `:421-423`) and the registry stores it per binding; same level/id with different vintages now yields two identities, order-independent (test `test_geography_vintage_splits_identity`). All 168 live observations carry `vintage: "current"`, so no live identity moved. - -**3. [HIGH] Auto source-concept aliases merge/flip concepts — fixed.** -`measure.source_concept` values are provenance now: recorded per row in a new `source_concepts` field, excluded from `aliases`, excluded from match names (`:817`, `:894`). Your `OFFICIAL_SHARED` repro: rate_b mints fresh, and A+B together stay two rows with no canonical flip (test `test_source_concept_never_drives_inheritance`). Row 104's derived share no longer aliases `fns.snap.total_payment_error_rate` — it cites it as `source_concepts` provenance. The one-time cleanup of the 85 machine aliases was done as reviewed data curation (not version-gated code); the two docket links that genuinely were identity statements — `bls.ces.average_hourly_earnings_private`, `statcan.employment_insurance.regular_beneficiaries` — were re-added as explicit curated aliases. `ambiguous_aliases` is now empty. The three alias-linked live pairs (initial claims, housing starts, industrial production) **deliberately remain separate rows**: with source labels demoted to provenance, no alias relation links them any more; folding each pair is a curation judgment (delete absorbed row + curated alias + `--allow-remint`, per the module docstring recipe) that should be its own reviewed change, not a mechanical side effect of this one. - -**4. [HIGH] Logical UUID collisions pass — fixed.** -Canonical lowercase form is required everywhere (`canonical_uuid_problem`, `:447-460`) and uniqueness keys on the parsed 128-bit value (`validate_uuids`, `:1035-1060`; `claim_uuid`, `:824-838`; registry load). Your uppercase/hyphenless/braced variants each produce two findings — non-canonical form and same-128-bit-value duplicate (test `test_uuid_validation_requires_canonical_and_parsed_uniqueness`). - -**5. [MEDIUM] Missing seed silently accepted — fixed.** -A missing seed path is a hard error in write and check modes alike (`:1190-1195`, test `test_main_missing_seed_is_a_hard_error`), and the committed-catalog test additionally pins `docket_seed_sha256 is not None`, seed existence, and the 201-row count. - -**6. [MEDIUM] Shape-pass strips mismatched/impossible tokens — fixed.** -Stripping is no longer "shape OR derived": a segment strips only when it is a direct spelling of the row's own period or parses to a calendar window **overlapping** that period (`family_pattern` + `_matches_period`, `:337-398`) — this covers all eight live shape-only strips (day-in-month, week-overlapping-month, `after_`-qualified month, month-range covering the period month; fiscal-year tokens must match the fiscal-year period exactly). `2025_12` on a 2026-06 row and impossible tokens like `2026_13` are **kept in the identity and flagged** in `suspect_segments` (tests `test_mismatched_tokens_flagged_not_stripped`, doctests). The false test comment about `june_2026` is gone. Bonus: fixed a latent v2 bug where `MONTHS_ABBREV` held 13 entries, silently shifting derived abbreviations for October–December rows. - -## Regenerated artifact (all v2 heals retained) - -201 series — 155 observed, 46 docket-only; FNS 54-way split (national row with FY2024+FY2025, 53 state rows); BoE one row, count 2; both M3 rows observed; Eurostat May-final (EA/household) and June-flash (EA21/economy) separate; `suspect_segments: []`; `ambiguous_aliases: []`; `minted=0, superseded=0` on the migration build. - -## Validation record - -```text -uv run pytest tests/test_build_series_catalog.py -q 63 passed -uv run ruff check script + tests clean -python3 -m doctest scripts/build_series_catalog.py clean -python3 scripts/build_series_catalog.py --check PASS (201) -identity continuity 2859ecb -> 3cb0c8a 201/201 UUIDs preserved -HEAD checker vs 6ab4fbe catalog copy FAILS (registry agreement, exit 1) -missing seed (write / check) FAILS (hard error, both) -uppercase / hyphenless / braced duplicate uuid FAILS validation -registry line edited FAILS --check + --verify-registry-append-only -catalog uuid edited, no flag build REFUSES, nothing written ---allow-remint without --remint-note REFUSES ---allow-remint + note writes chained supersede line; --check green after -seed entry dropped, no flag build REFUSES -byte idempotence (catalog + registry) PASS -``` - -Registry bootstrap is reproducible: one mint line per `2859ecb` catalog row, in row order (script preserved at `~/thesis-wave-0731/catalog-v3-migration.py`; the agreement check makes the equivalence machine-verifiable). - -A third adversarial review is being dispatched against `3cb0c8a`. - ---- - -## Addendum (45fcc10, self-found before the third review) - -Rebuilding from a bare registry (committed catalog deleted/empty) loses -curated naming/alias memory; after any future rename curation the renamed -identity would re-key away from its binding and silently fresh-mint, -passing agreement and append-only checks. The builder now refuses a -bare-registry rebuild absent --allow-remint --remint-note -(test_rebuild_without_prior_catalog_is_gated; suite is 64 tests). - ---- - -## Addendum 2 (2c31ae4 — four findings from your predecessor's crashed run, all fixed) - -Attempt 1 of the third review crashed before its REPORT after finding: -(1) lossy pipe-joined dimension keys (now JSON-encoded, injective); -(2) partial-catalog deletion silently re-minting (registry now tracks -liveness with retired/revived events; live-binding uuids must appear in -the catalog — both directions checked); (3) silent overlap strips (new -overlap_stripped_segments audit header, currently the 8 live segments); -(4) CI append-only gate PR-only (now also push events via -github.event.before, loud on unfetchable base). Also: the migration -removed 82 machine alias instances (disposition said 85 — prose error). From 31e895182a096cb38320b4d0fa8d5a7d8a3ce63d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 2 Aug 2026 12:37:42 -0400 Subject: [PATCH 09/11] Answer fourth adversarial review: lineage is linear, audits carry context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All six findings addressed; identity map unchanged again (201/201, minted=0, superseded=0): 1. [CRITICAL] Superseding UUIDs must be new to the registry outright — same-identity U1->U2->U1 cycles that restored the anchor while hiding the excursion are rejected at load and at write. 2. [HIGH] succeeds lineage is linear and shape-bound: a retired predecessor is consumed exactly once (forks rejected), must share the successor's concept, must be entity-less, and must match on geography level/id (and vintage when declared); the builder's enrichment branch enforces the same shape before planning events, so status-flipped committed rows cannot smuggle a UUID across dimensions. 3. [MEDIUM] A literal "{P}" segment in observation identifiers is a reserved-placeholder hard error instead of a silent identity collapse. 4. [MEDIUM] stripped_segments is now an occurrence map (spelling -> sorted canonical concepts touched), so a second identity absorbing the same spelling is visible instead of vanishing into a set. 5. [MEDIUM] Calendar tokens live in a 1900-2999 window: fy0000/0000_01/ week_ending_0001_01_01 neither strip nor crash; annual rows gain bare-year direct variants so year-suffixed annual ids no longer split per year. 6. [MEDIUM] The all-zero-push fallback's trust anchor is documented: branch protection on codex/thesis-ledger-facts forbids deletions and force pushes (verified live via the GitHub API, which the review sandbox could not reach). Also raises the supersede/retire note floor to real content (>=8 chars, >=4 alphanumeric). 90 tests; ruff, doctest, --check, byte-idempotence green. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 4 + ledger/series_catalog.json | 207 +++++++++++++++++++++++++---- scripts/build_series_catalog.py | 133 +++++++++++++++--- tests/test_build_series_catalog.py | 157 ++++++++++++++++++++-- 4 files changed, 447 insertions(+), 54 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 217343a..7957c9b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,6 +57,10 @@ jobs: || github.event.before }} # New-branch pushes have an all-zero before-sha; they must still # extend the canonical lineage's registry rather than skipping. + # Trust anchor: this fallback ref is only sound because branch + # protection on codex/thesis-ledger-facts forbids deletions and + # force pushes (verified 2026-08-02); a recreated ref would + # otherwise compare the registry to itself. FALLBACK_REF: codex/thesis-ledger-facts run: | if [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then diff --git a/ledger/series_catalog.json b/ledger/series_catalog.json index 01f92f9..6955c2d 100644 --- a/ledger/series_catalog.json +++ b/ledger/series_catalog.json @@ -6,35 +6,184 @@ "docket_seed_sha256": "930424fb48c0be4c9e2ce17d4e0f2a6be886408e814c80324174a7a303fa0271", "uuid_registry_sha256": "c4ccda3f1746ff06cf8cc17dc221ea3a82b7b16a5d719361e2025bc5d7b63356", "suspect_segments": [], - "stripped_segments": [ - "2026-05", - "2026-06", - "2026-06-18", - "2026-07", - "2026_05", - "2026_06", - "2026_06_18", - "2026_q2", - "after_june_2026", - "after_mpc_june_2026", - "april_2026", - "feb_2026", - "february_to_april_2026", - "fy2024", - "fy2025", - "june_2026", - "may_2026", - "q1_2026", - "week_2026-06-13", - "week_2026-06-20", - "week_2026-06-27", - "week_2026-07-04", - "week_2026-07-11", - "week_2026-07-18", - "week_2026-07-25", - "week_2026_06_13", - "week_ending_2026_06_06" - ], + "stripped_segments": { + "2026-05": [ + "abs.cpi_indicator.allgroups.yoy", + "estat.jp.cpi.core_exfreshfood.yoy", + "ons.cpih.annual_rate", + "statcan.36-10-0434-01.all_industries.month_to_month_percent_change", + "statcan.cpi.allitems.yoy", + "us.bea.core_pce.mom_sa", + "us.census.housing_starts.total_saar", + "us.frb.industrial_production.total.mom_sa" + ], + "2026-06": [ + "abs.cpi.all_groups.yoy", + "bls.import_price_index.all_imports_mom", + "census.housing_starts.saar", + "eurostat.ea.hicp.flash.yoy", + "fed.g17.capacity_utilization.total_industry", + "fed.g17.industrial_production.total_index_mom", + "us.bea.core_pce.mom_sa", + "us.fed.fomc.target_range_upper" + ], + "2026-06-18": [ + "boe.bank_rate" + ], + "2026-07": [ + "cms.care_compare.nursing_home_occupancy_pct", + "cms.nursing_home_compare.reported_total_nurse_staffing_hprd_us", + "eurostat.ea.hicp.flash.yoy" + ], + "2026_05": [ + "estat.jp.cpi.core_exfreshfood.yoy", + "ons.cpih.annual_rate", + "us.census.housing_starts.total_saar", + "us.frb.industrial_production.total.mom_sa" + ], + "2026_06": [ + "census.m3.durable_goods_new_orders_mom", + "census.m3.durable_goods_shipments_mom", + "us.fed.fomc.target_range_upper" + ], + "2026_06_18": [ + "boe.bank_rate" + ], + "2026_q2": [ + "bls.eci.private_wages_salaries_qoq", + "bls.eci.total_compensation_private_industry_qoq" + ], + "after_june_2026": [ + "bank_of_canada.overnight_rate", + "boj.policy_rate_guideline", + "ecb.deposit_facility_rate", + "rba.cash_rate_target" + ], + "after_mpc_june_2026": [ + "boe.bank_rate" + ], + "april_2026": [ + "census.mtis.total_business_inventories_level", + "eurostat.industrial_production.euro_area", + "ons.gdp.monthly_growth", + "statcan.building_permits.total_value_mom.canada", + "statcan.employment_insurance.regular_beneficiaries.canada", + "statcan.gdp_by_industry.monthly_growth", + "statcan.retail_trade.sales_mom.canada", + "statcan.wholesale_trade.sales_mom_exclusions.canada" + ], + "feb_2026": [ + "cms.medicaid_pi.beneficiaries_disenrolled_procedural", + "cms.medicaid_pi.beneficiaries_disenrolled_total", + "cms.medicaid_pi.beneficiaries_renewed_ex_parte", + "cms.medicaid_pi.beneficiaries_renewed_total" + ], + "february_to_april_2026": [ + "ons.labour.unemployment_rate" + ], + "fy2024": [ + "fns.snap.application_processing_timeliness_rate", + "fns.snap.overpayment_error_rate", + "fns.snap.total_payment_error_rate", + "fns.snap.underpayment_error_rate" + ], + "fy2025": [ + "fns.snap.share_jurisdictions_at_or_above_6pct", + "fns.snap.total_payment_error_rate" + ], + "june_2026": [ + "abs.labour.employment_change.australia", + "abs.labour.unemployment_rate.australia", + "bls.ces.total_nonfarm_payroll_change", + "bls.cpi.u.core_mom", + "bls.cpi.u.headline_mom", + "bls.cps.employed_people_by_occupation.business_financial_operations", + "bls.cps.employed_people_by_occupation.computer_mathematical", + "bls.cps.employed_people_by_occupation.healthcare_support", + "bls.cps.employed_people_by_occupation.office_administrative_support", + "bls.cps.employed_people_by_occupation.production", + "bls.cps.employed_people_by_occupation.transportation_material_moving", + "bls.cps.unemployment_rate", + "eurostat.hicp.all_items_annual_rate.euro_area", + "statjp.cpi.tokyo_all_items_annual_rate" + ], + "may_2026": [ + "abs.building_approvals.total_dwellings_mom.australia", + "abs.cpi.all_groups_annual_rate.australia", + "abs.labour.employment_change.australia", + "abs.labour.unemployment_rate.australia", + "bea.disposable_personal_income.level", + "bea.government_social_benefits.level", + "bea.government_social_benefits.medicaid", + "bea.government_social_benefits.medicare", + "bea.government_social_benefits.social_security", + "bea.pce.core_mom", + "bea.pce_price_index.monthly_change", + "bea.personal_current_taxes.level", + "bea.wages_and_salaries.level", + "bls.ces.average_hourly_earnings_private_monthly_change", + "bls.ces.total_nonfarm_payroll_change", + "bls.cpi.u.core_mom", + "bls.cpi.u.headline_mom", + "bls.cps.unemployment_rate", + "bls.import_price_index.all_imports_mom", + "bls.jolts.job_openings", + "bls.jolts.job_openings_total", + "bls.ppi.final_demand_monthly_change", + "census.housing_starts.saar", + "census.marts.adv44x72.monthly_change", + "eurostat.hicp.all_items_annual_rate.euro_area", + "eurostat.retail_trade.volume_mom.euro_area", + "eurostat.unemployment_rate.euro_area", + "fed.g17.capacity_utilization.total_industry", + "fed.g17.industrial_production.total_index_mom", + "ons.cpi.annual_rate", + "ons.hmrc.paye_payrolled_employees", + "ons.pusf.j5ii.public_sector_net_borrowing_ex_banks", + "ons.retail_sales.volume_mom", + "statcan.cpi.all_items_annual_rate.canada", + "statcan.employment_insurance.regular_beneficiaries.canada", + "statcan.lfs.employment_change", + "statcan.lfs.unemployment_rate", + "statjp.cpi.all_items_annual_rate.japan", + "statjp.household_spending.real_yoy.two_or_more_person_households", + "statjp.lfs.unemployment_rate.japan", + "treasury.mts.monthly_deficit" + ], + "q1_2026": [ + "bea.real_gdp.saar.third_estimate" + ], + "week_2026-06-13": [ + "us.dol.initial_claims.sa" + ], + "week_2026-06-20": [ + "us.dol.initial_claims.sa" + ], + "week_2026-06-27": [ + "dol.eta.continued_claims.sa" + ], + "week_2026-07-04": [ + "dol.eta.continued_claims.sa", + "us.dol.initial_claims.sa" + ], + "week_2026-07-11": [ + "dol.eta.continued_claims.sa", + "us.dol.initial_claims.sa" + ], + "week_2026-07-18": [ + "dol.eta.continued_claims.sa", + "us.dol.initial_claims.sa" + ], + "week_2026-07-25": [ + "us.dol.initial_claims.sa" + ], + "week_2026_06_13": [ + "us.dol.initial_claims.sa" + ], + "week_ending_2026_06_06": [ + "dol.eta.initial_claims.sa" + ] + }, "ambiguous_aliases": [], "series": [ { diff --git a/scripts/build_series_catalog.py b/scripts/build_series_catalog.py index 06be639..75eaffc 100644 --- a/scripts/build_series_catalog.py +++ b/scripts/build_series_catalog.py @@ -153,6 +153,15 @@ _YEAR_HINT = re.compile(r"(?:19|20)\d{2}") +# Calendar tokens are only meaningful in a sane modern window; anything +# outside neither strips nor crashes date arithmetic (fy0000, 0000_01, +# week_ending_0001_01_01 were previously accepted or raised). +_YEAR_MIN, _YEAR_MAX = 1900, 2999 + + +def _valid_year(year: int) -> bool: + return _YEAR_MIN <= year <= _YEAR_MAX + _FY_RE = re.compile(r"fy(\d{4})") _NUMERIC_DATE_RE = re.compile(r"(\d{4})[-_](\d{2})(?:[-_](\d{2}))?") _MONTH_NAME_RE = re.compile(r"(%s)_(\d{4})" % _MONTH_ALT) @@ -162,13 +171,15 @@ def _month_span(year: int, month: int) -> tuple[dt.date, dt.date] | None: - if not 1 <= month <= 12: + if not _valid_year(year) or not 1 <= month <= 12: return None last = calendar.monthrange(year, month)[1] return dt.date(year, month, 1), dt.date(year, month, last) def _day(year: int, month: int, day: int) -> dt.date | None: + if not _valid_year(year): + return None try: return dt.date(year, month, day) except ValueError: @@ -210,7 +221,8 @@ def parse_period_token(segment: str) -> tuple | None: return None m = _FY_RE.fullmatch(segment) if m: - return ("fiscal_year", int(m.group(1))) + year = int(m.group(1)) + return ("fiscal_year", year) if _valid_year(year) else None m = _WEEK_RE.fullmatch(segment) if m: end = _day(int(m.group(1)), int(m.group(2)), int(m.group(3))) @@ -229,7 +241,7 @@ def parse_period_token(segment: str) -> tuple | None: if m: first, last = _MONTH_NUM[m.group(1)], _MONTH_NUM[m.group(2)] year = int(m.group(3)) - if first > last: + if first > last or not _valid_year(year): return None start = dt.date(year, first, 1) end = _month_span(year, last)[1] @@ -241,6 +253,8 @@ def parse_period_token(segment: str) -> tuple | None: if m: quarter = int(m.group(1) or m.group(4)) year = int(m.group(2) or m.group(3)) + if not _valid_year(year): + return None start = dt.date(year, 3 * quarter - 2, 1) end = _month_span(year, 3 * quarter)[1] return ("span", (start, end)) @@ -274,7 +288,15 @@ def period_token_variants(period: dict) -> set[str]: return tokens value = str(value) if ptype == "fiscal_year": - tokens.add(f"fy{value}") + if value.isdigit() and _valid_year(int(value)): + tokens.add(f"fy{value}") + elif ptype == "year": + # Bare years are deliberately not in the token grammar (too + # collision-prone to strip by shape), but a segment spelling the + # row's OWN annual period is a direct variant and must strip, or + # year-suffixed annual ids split into per-year identities. + if value.isdigit() and _valid_year(int(value)): + tokens.add(value) elif ptype == "month": m = re.fullmatch(r"(\d{4})-(\d{2})", value) if m: @@ -311,7 +333,7 @@ def period_descriptor(period: dict | None) -> tuple | None: return None value = str(value) if ptype == "fiscal_year": - if value.isdigit(): + if value.isdigit() and _valid_year(int(value)): return ("fiscal_year", int(value)) return None if ptype == "month": @@ -339,7 +361,7 @@ def period_descriptor(period: dict | None) -> tuple | None: return ("span", (end - dt.timedelta(days=6), end)) return None if ptype == "year": - if value.isdigit(): + if value.isdigit() and _valid_year(int(value)): year = int(value) return ("span", (dt.date(year, 1, 1), dt.date(year, 12, 31))) return None @@ -523,6 +545,13 @@ def build_identities(rows: list[dict]) -> dict[tuple[str, str, str], dict]: f"measure.concept: {json.dumps(row)[:200]}" ) period = row.get("period") or {} + for identifier in (concept_raw, rid): + if "{P}" in identifier.split("."): + raise SystemExit( + f"observation row {index} contains the reserved " + f"placeholder segment '{{P}}' in {identifier!r} — " + "family patterns are derived, never supplied" + ) if ( period.get("value") is not None and period_descriptor(period) is None @@ -698,8 +727,10 @@ def _reject_duplicate_keys(pairs: list[tuple[str, object]]) -> dict: def _usable_note(note: object) -> bool: - """A note must carry visible content, not just zero-width padding.""" - return isinstance(note, str) and any(c.isalnum() for c in note) + """A note must say something: several words' worth of real content.""" + if not isinstance(note, str): + return False + return len(note.strip()) >= 8 and sum(c.isalnum() for c in note) >= 4 class UuidRegistry: @@ -737,6 +768,9 @@ def __init__(self, path: pathlib.Path, raw: bytes) -> None: self.latest: dict[tuple[str, str, str], dict] = {} # First owner of each 128-bit value, for the no-reuse rule. self.uuid_owner: dict[int, tuple[str, str, str]] = {} + # Retired predecessors already consumed by a succeeds event: a + # lineage can be handed over exactly once, never forked. + self.consumed: set[tuple[str, str, str]] = set() problems: list[str] = [] if b"\r" in raw: problems.append("registry must be LF-only (CR byte found)") @@ -812,11 +846,16 @@ def __init__(self, path: pathlib.Path, raw: bytes) -> None: f"line {lineno}: supersede for {key} is a no-op " "(uuid equals supersedes)" ) - elif self.uuid_owner.get(parsed) not in (None, key): + elif parsed in self.uuid_owner: + # Never revisit a historical value, even for the same + # identity: U1 -> U2 -> U1 cycles would let the final + # map and the identity anchor "return to normal" while + # hiding the excursion. problems.append( - f"line {lineno}: supersede for {key} reuses uuid " - f"{entry['uuid']} already bound to " - f"{self.uuid_owner[parsed]}" + f"line {lineno}: supersede for {key} recycles uuid " + f"{entry['uuid']} (first bound to " + f"{self.uuid_owner[parsed]}) — superseding UUIDs " + "must be new to the registry" ) if not _usable_note(entry.get("note")): problems.append( @@ -895,11 +934,54 @@ def _succeeds_problem( f"line {lineno}: {key} succeeds a LIVE binding " f"{predecessor_key} — retire it first" ) + if predecessor_key in self.consumed: + return ( + f"line {lineno}: {key} succeeds {predecessor_key}, whose " + "lineage was already handed over — a predecessor is " + "consumed exactly once, never forked" + ) if predecessor["uuid"] != entry["uuid"]: return ( f"line {lineno}: {key} succeeds {predecessor_key} but " f"carries uuid {entry['uuid']} != {predecessor['uuid']}" ) + # succeeds exists for exactly one documented move — a docket + # placeholder enriched into its observed identity — so it must + # look like one: same concept, predecessor entity unknown, + # geography absent or matching on level/id (and vintage when + # declared). + if succeeds.get("concept") != entry["concept"]: + return ( + f"line {lineno}: {key} succeeds a different concept " + f"{succeeds.get('concept')!r} — lineage never crosses " + "concepts" + ) + if succeeds.get("entity") is not None: + return ( + f"line {lineno}: {key} succeeds an identity with a known " + "entity — only entity-less placeholders can be enriched" + ) + pred_geo = succeeds.get("geography") or None + if pred_geo is not None: + entry_geo = entry.get("geography") or {} + if (pred_geo.get("level"), pred_geo.get("id")) != ( + entry_geo.get("level"), + entry_geo.get("id"), + ): + return ( + f"line {lineno}: {key} succeeds an identity in a " + "different geography — lineage never crosses " + "level/id" + ) + pred_vintage = pred_geo.get("vintage") + if pred_vintage is not None and pred_vintage != entry_geo.get( + "vintage" + ): + return ( + f"line {lineno}: {key} succeeds an identity with a " + "conflicting geography vintage" + ) + self.consumed.add(predecessor_key) return None @staticmethod @@ -1137,9 +1219,24 @@ def resolve_uuid( prior_own_key = ( UuidRegistry.entry_key(prior) if prior is not None else None ) - if ( + prior_geo = (prior or {}).get("geography") or None + geo = geography or {} + enrichment_shaped = ( prior is not None and prior.get("status") == "docket-only" + and prior.get("entity") is None + and ( + prior_geo is None + or ( + (prior_geo.get("level"), prior_geo.get("id")) + == (geo.get("level"), geo.get("id")) + and prior_geo.get("vintage") + in (None, geo.get("vintage")) + ) + ) + ) + if ( + enrichment_shaped and owner_key == prior_own_key and registry.is_live(owner_key) ): @@ -1188,7 +1285,7 @@ def resolve_uuid( return row_uuid all_suspects: set[str] = set() - all_stripped: set[str] = set() + stripped_map: dict[str, set[str]] = {} for canon_key in sorted(canonical): bucket = canonical[canon_key] concept, _, _ = canon_key @@ -1200,7 +1297,8 @@ def resolve_uuid( curated_aliases = set(prior.get("aliases", [])) if prior else set() aliases = sorted((bucket["concepts"] | curated_aliases) - {concept}) all_suspects.update(bucket["suspects"]) - all_stripped.update(bucket["stripped"]) + for segment in bucket["stripped"]: + stripped_map.setdefault(segment, set()).add(concept) series.append({ "uuid": row_uuid, "concept": concept, @@ -1388,7 +1486,10 @@ def resolve_uuid( ), "uuid_registry_sha256": None, "suspect_segments": sorted(all_suspects), - "stripped_segments": sorted(all_stripped), + "stripped_segments": { + segment: sorted(stripped_map[segment]) + for segment in sorted(stripped_map) + }, "ambiguous_aliases": ambiguous_aliases, "series": series, } diff --git a/tests/test_build_series_catalog.py b/tests/test_build_series_catalog.py index 30031bf..ae7b464 100644 --- a/tests/test_build_series_catalog.py +++ b/tests/test_build_series_catalog.py @@ -748,9 +748,10 @@ def test_committed_catalog_is_current_and_valid() -> None: assert bsc.validate_uuids(committed) == [] assert bsc.registry_agreement_problems(committed, registry) == [] assert committed["suspect_segments"] == [] - # EVERY stripped spelling is auditable — a statute or edition label - # that collides with a period spelling can only be caught here. - assert committed["stripped_segments"] == [ + # EVERY stripped spelling is auditable, mapped to the canonical + # concepts it touched — a statute or edition label colliding with a + # period spelling can only be caught here. + assert sorted(committed["stripped_segments"]) == [ "2026-05", "2026-06", "2026-06-18", "2026-07", "2026_05", "2026_06", "2026_06_18", "2026_q2", "after_june_2026", "after_mpc_june_2026", "april_2026", "feb_2026", @@ -760,6 +761,13 @@ def test_committed_catalog_is_current_and_valid() -> None: "week_2026-07-18", "week_2026-07-25", "week_2026_06_13", "week_ending_2026_06_06", ] + assert committed["stripped_segments"]["after_mpc_june_2026"] == [ + "boe.bank_rate" + ] + assert all( + occurrences and occurrences == sorted(set(occurrences)) + for occurrences in committed["stripped_segments"].values() + ) assert committed["docket_seed_sha256"] is not None assert committed["uuid_registry_sha256"] == registry.sha256() assert bsc.DOCKET_SEED.exists() @@ -1015,14 +1023,22 @@ def test_statute_spelling_stripped_but_audited( ) -> None: # A statute/edition label spelling the row's own period is # indistinguishable from a period label; it strips, but the spelling - # is published for curation rather than vanishing. + # is published — mapped to every canonical concept it touched, so a + # second occurrence is visible rather than absorbed into a set. catalog, _ = _build( tmp_path, - [_row("agency.statute.2026_05.rate", - rid="agency.statute.2026_05.rate.first_print")], + [ + _row("agency.statute.2026_05.rate", + rid="agency.statute.2026_05.rate.first_print"), + _row("other.report.2026_05.level", + rid="other.report.2026_05.level.first_print"), + ], ) - assert catalog["series"][0]["concept"] == "agency.statute.rate" - assert "2026_05" in catalog["stripped_segments"] + concepts = {r["concept"] for r in catalog["series"]} + assert concepts == {"agency.statute.rate", "other.report.level"} + assert catalog["stripped_segments"]["2026_05"] == [ + "agency.statute.rate", "other.report.level", + ] @pytest.mark.parametrize( @@ -1052,7 +1068,9 @@ def test_remint_of_retired_identity_stages_valid_chain( (tmp_path / "seed.json").write_text( json.dumps({"series": []}), encoding="utf-8" ) - assert bsc.main(argv + ["--allow-remint", "--remint-note", "cut"]) == 0 + assert bsc.main( + argv + ["--allow-remint", "--remint-note", "seed entry cut"] + ) == 0 (tmp_path / "seed.json").write_text(json.dumps(SEED), encoding="utf-8") catalog = json.loads((tmp_path / "catalog.json").read_text()) # Hand-plant a divergent uuid for the returning docket identity. @@ -1119,3 +1137,124 @@ def test_identity_uuid_map_matches_reviewed_anchor() -> None: assert digest == ( "2d2552e88662eb654597999ac92f750cadbed882703324c6f9ca1f1e3ac15345" ) + + +def test_supersede_never_recycles_a_historical_uuid( + tmp_path: pathlib.Path, +) -> None: + # Fourth-review repro: U1 -> U2 -> U1 restored the original map while + # hiding the excursion in two "valid" events. + path = tmp_path / "registry.jsonl" + mint = _mint("a.one", U1) + away = dict(_mint("a.one", U2), supersedes=U1, note="planned change one") + back = dict(_mint("a.one", U1), supersedes=U2, note="planned change two") + path.write_text( + "".join(json.dumps(e) + "\n" for e in (mint, away, back)), + encoding="utf-8", + ) + with pytest.raises(SystemExit, match="recycles uuid"): + bsc.UuidRegistry.load(path) + + +def test_succeeds_lineage_is_consumed_once_and_dimension_bound( + tmp_path: pathlib.Path, +) -> None: + path = tmp_path / "registry.jsonl" + mint = _mint("a.one", U1) + retire = dict(_mint("a.one", U1), retired=True, note="placeholder done") + succ = {"concept": "a.one", "geography": None, "entity": None} + + def entry(concept, uuid, **kw): + return dict(_mint(concept, uuid), **kw) + + # Fork: two successors of one predecessor. + b = entry("a.one", U1, succeeds=succ, + entity={"name": "economy", "role": "aggregate"}) + b_away = dict( + entry("a.one", U2, entity={"name": "economy", "role": "aggregate"}), + supersedes=U1, note="moved along again", + ) + c = entry("a.one", U1, succeeds=succ, + entity={"name": "person", "role": "aggregate"}) + path.write_text( + "".join(json.dumps(e) + "\n" for e in (mint, retire, b, b_away, c)), + encoding="utf-8", + ) + with pytest.raises(SystemExit, match="consumed exactly once"): + bsc.UuidRegistry.load(path) + + # Cross-concept lineage transfer. + other = entry("b.two", U1, succeeds=succ) + path.write_text( + "".join(json.dumps(e) + "\n" for e in (mint, retire, other)), + encoding="utf-8", + ) + with pytest.raises(SystemExit, match="never crosses concepts"): + bsc.UuidRegistry.load(path) + + # Predecessor with a known entity is not a placeholder. + known = _mint("a.one", U1, + entity={"name": "economy", "role": "aggregate"}) + known_retire = dict(known, retired=True, note="placeholder done") + successor = entry( + "a.one", U1, + succeeds={"concept": "a.one", "geography": None, + "entity": {"name": "economy", "role": "aggregate"}}, + ) + path.write_text( + "".join( + json.dumps(e) + "\n" for e in (known, known_retire, successor) + ), + encoding="utf-8", + ) + with pytest.raises(SystemExit, match="entity-less placeholders"): + bsc.UuidRegistry.load(path) + + +def test_literal_placeholder_segment_is_reserved( + tmp_path: pathlib.Path, +) -> None: + with pytest.raises(SystemExit, match="reserved placeholder"): + _build(tmp_path, [_row("agency.statute.{P}.rate")]) + with pytest.raises(SystemExit, match="reserved placeholder"): + _build( + tmp_path, + [_row("agency.rate", rid="agency.rate.{P}.first_print")], + ) + + +@pytest.mark.parametrize( + "segment", + ["fy0000", "fy0001", "0000_01", "week_ending_0001_01_01", "q1_0000"], +) +def test_out_of_window_calendar_tokens_neither_strip_nor_crash( + segment: str, +) -> None: + assert bsc.parse_period_token(segment) is None + pattern = bsc.family_pattern( + f"agency.rate.{segment}", {"type": "month", "value": "2026-05"} + ) + assert pattern == f"agency.rate.{segment}" + + +def test_bare_year_variant_strips_only_for_annual_rows( + tmp_path: pathlib.Path, +) -> None: + rows = [ + _row("agency.annual.total.2025", + rid="agency.annual.total.2025.final", + period={"type": "year", "value": "2025"}), + _row("agency.annual.total.2026", + rid="agency.annual.total.2026.final", + period={"type": "year", "value": "2026"}), + ] + catalog, _ = _build(tmp_path, rows) + assert [r["concept"] for r in catalog["series"]] == [ + "agency.annual.total" + ] + assert catalog["series"][0]["observation_count"] == 2 + # A bare year that is NOT the row's own period never strips. + pattern = bsc.family_pattern( + "agency.annual.total.2019", {"type": "year", "value": "2025"} + ) + assert pattern == "agency.annual.total.2019" From 508106df53bf55cc2ee9d106a46ae4b95b725539 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 2 Aug 2026 13:00:44 -0400 Subject: [PATCH 10/11] Answer fifth adversarial review: consumed lineage is terminal, inputs are strict MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All four findings addressed; catalog and registry bytes unchanged (201/201, minted=0, superseded=0): 1. [CRITICAL] A handed-over lineage is terminal: reviving a consumed predecessor is rejected at load, the builder mints FRESH for observations reappearing under a consumed identity (never revives), the writer refuses supersede replacements the registry has ever seen, and live-UUID uniqueness now holds after EVERY event prefix, not just the final state — the retire/succeed/supersede/revive detour that recreated a banned U1->U2->U1 excursion is closed at four layers. 2. [HIGH] The reserved '{P}' guard is central: observation identifiers, docket series names, and registry concepts (including succeeds targets) all reject the placeholder segment. 3. [MEDIUM] Calendar clamp completed: month-name tokens and quarter period descriptors guard the 1900-2999 window (january_1899 / january_3000 / quarter 1899-01 / 3000-01 neither strip nor crash), and the year-hint widens to 1800-3099 so impossible out-of-window tokens (1899_13, 2999_13, 3000_13) are suspect-flagged. 4. [MEDIUM] Strict JSON and identity domains: NaN/Infinity rejected on every parse (registry lines and observations) and every render (allow_nan=False), and geography/entity fields must be null or NONEMPTY strings with known keys — an empty-string vintage can no longer coexist as a distinct identity beside an absent one, in the registry or in observations. 103 tests; ruff, doctest, --check, byte-idempotence green. Co-Authored-By: Claude Fable 5 --- scripts/build_series_catalog.py | 162 +++++++++++++++++++++++------ tests/test_build_series_catalog.py | 161 ++++++++++++++++++++++++++++ 2 files changed, 293 insertions(+), 30 deletions(-) diff --git a/scripts/build_series_catalog.py b/scripts/build_series_catalog.py index 75eaffc..9436cc9 100644 --- a/scripts/build_series_catalog.py +++ b/scripts/build_series_catalog.py @@ -151,7 +151,9 @@ "BE": {"level": "country", "id": "BE", "name": None}, } -_YEAR_HINT = re.compile(r"(?:19|20)\d{2}") +# Wide enough to flag plausible-but-out-of-window years (1899_13, +# 2999_13, 3000_13) without tripping on catalog table ids like 0434. +_YEAR_HINT = re.compile(r"(?:1[89]\d{2}|2\d{3}|30\d{2})") # Calendar tokens are only meaningful in a sane modern window; anything # outside neither strips nor crashes date arithmetic (fy0000, 0000_01, @@ -248,7 +250,8 @@ def parse_period_token(segment: str) -> tuple | None: return ("span", (start, end)) m = _MONTH_NAME_RE.fullmatch(segment) if m: - return ("span", _month_span(int(m.group(2)), _MONTH_NUM[m.group(1)])) + span = _month_span(int(m.group(2)), _MONTH_NUM[m.group(1)]) + return ("span", span) if span else None m = _QUARTER_RE.fullmatch(segment) if m: quarter = int(m.group(1) or m.group(4)) @@ -346,7 +349,7 @@ def period_descriptor(period: dict | None) -> tuple | None: m = re.fullmatch(r"(\d{4})-(\d{2})", value) if m: year, month = int(m.group(1)), int(m.group(2)) - if not 1 <= month <= 12: + if not _valid_year(year) or not 1 <= month <= 12: return None quarter = (month - 1) // 3 + 1 start = dt.date(year, 3 * quarter - 2, 1) @@ -546,12 +549,16 @@ def build_identities(rows: list[dict]) -> dict[tuple[str, str, str], dict]: ) period = row.get("period") or {} for identifier in (concept_raw, rid): - if "{P}" in identifier.split("."): - raise SystemExit( - f"observation row {index} contains the reserved " - f"placeholder segment '{{P}}' in {identifier!r} — " - "family patterns are derived, never supplied" - ) + reserved = _reserved_segment_problem(identifier) + if reserved: + raise SystemExit(f"observation row {index}: {reserved}") + for what, allowed in ( + ("geography", ("level", "id", "vintage", "name")), + ("entity", ("name", "role")), + ): + domain = _dimension_problem(row.get(what) or None, allowed, what) + if domain: + raise SystemExit(f"observation row {index}: {domain}") if ( period.get("value") is not None and period_descriptor(period) is None @@ -712,6 +719,41 @@ def match( return None +def _reserved_segment_problem(identifier: object) -> str | None: + """Why an identifier is unusable as a concept/series name, else None.""" + if not isinstance(identifier, str) or not identifier: + return f"identifier {identifier!r} must be a nonempty string" + if "{P}" in identifier.split("."): + return ( + f"identifier {identifier!r} contains the reserved placeholder " + "segment '{P}' — family patterns are derived, never supplied" + ) + return None + + +def _dimension_problem(value: object, allowed: tuple[str, ...], + what: str) -> str | None: + """Geography/entity objects: null, or known keys with nonempty/null + string values (an empty string must never be a distinct identity from + an absent field).""" + if value is None: + return None + if not isinstance(value, dict): + return f"{what} must be an object or null" + for field, field_value in value.items(): + if field not in allowed: + return f"{what}.{field} is not an identity field" + if field_value is not None and ( + not isinstance(field_value, str) or not field_value + ): + return f"{what}.{field} must be a nonempty string or null" + return None + + +def _reject_json_constants(value: str): + raise ValueError(f"JSON constant {value} is not allowed") + + def _reject_duplicate_keys(pairs: list[tuple[str, object]]) -> dict: """JSON object hook that refuses duplicate member names. @@ -766,6 +808,7 @@ def __init__(self, path: pathlib.Path, raw: bytes) -> None: self.raw = raw self.entries: list[dict] = [] self.latest: dict[tuple[str, str, str], dict] = {} + live_by_uuid: dict[int, tuple[str, str, str]] = {} # First owner of each 128-bit value, for the no-reuse rule. self.uuid_owner: dict[int, tuple[str, str, str]] = {} # Retired predecessors already consumed by a succeeds event: a @@ -781,15 +824,35 @@ def __init__(self, path: pathlib.Path, raw: bytes) -> None: problems.append(f"line {lineno}: blank line") continue try: - entry = json.loads(line, object_pairs_hook=_reject_duplicate_keys) + entry = json.loads( + line, + object_pairs_hook=_reject_duplicate_keys, + parse_constant=_reject_json_constants, + ) except (json.JSONDecodeError, ValueError) as exc: problems.append(f"line {lineno}: not strict JSON ({exc})") continue - if not isinstance(entry, dict) or not isinstance( - entry.get("concept"), str - ): - problems.append(f"line {lineno}: missing concept") + if not isinstance(entry, dict): + problems.append(f"line {lineno}: not an object") continue + reserved = _reserved_segment_problem(entry.get("concept")) + if reserved: + problems.append(f"line {lineno}: {reserved}") + continue + for what, allowed in ( + ("geography", ("level", "id", "vintage")), + ("entity", ("name", "role")), + ): + domain = _dimension_problem(entry.get(what), allowed, what) + if domain: + problems.append(f"line {lineno}: {domain}") + succeeds_field = entry.get("succeeds") + if isinstance(succeeds_field, dict): + reserved = _reserved_segment_problem( + succeeds_field.get("concept") + ) + if reserved: + problems.append(f"line {lineno}: succeeds {reserved}") problem = canonical_uuid_problem(entry.get("uuid")) if problem: problems.append(f"line {lineno}: {problem}") @@ -886,6 +949,14 @@ def __init__(self, path: pathlib.Path, raw: bytes) -> None: problems.append( f"line {lineno}: {key} revives a live binding" ) + if key in self.consumed: + # A handed-over lineage is terminal: reviving it would + # re-open the predecessor and recreate a banned + # U1 -> U2 -> U1 excursion through an identity detour. + problems.append( + f"line {lineno}: {key} revives a consumed " + "lineage — handed-over predecessors are terminal" + ) if entry["uuid"] != previous["uuid"]: problems.append( f"line {lineno}: revive for {key} must keep uuid " @@ -896,21 +967,25 @@ def __init__(self, path: pathlib.Path, raw: bytes) -> None: f"line {lineno}: {key} re-binds without supersedes " f"(prior uuid {previous['uuid']})" ) + previous_entry = self.latest.get(key) + if previous_entry is not None: + prev_parsed = uuid_module.UUID(previous_entry["uuid"]).int + if not self._is_retired(previous_entry) and ( + live_by_uuid.get(prev_parsed) == key + ): + del live_by_uuid[prev_parsed] self.entries.append(entry) self.latest[key] = entry self.uuid_owner.setdefault(parsed, key) - live_by_uuid: dict[int, tuple[str, str, str]] = {} - for key, entry in self.latest.items(): - if self._is_retired(entry): - continue - parsed = uuid_module.UUID(entry["uuid"]).int - other = live_by_uuid.get(parsed) - if other is not None: - problems.append( - f"live bindings {other} and {key} share uuid " - f"{entry['uuid']} — retire or supersede one explicitly" - ) - live_by_uuid[parsed] = key + if not self._is_retired(entry): + other = live_by_uuid.get(parsed) + if other is not None and other != key: + problems.append( + f"line {lineno}: live bindings {other} and {key} " + f"share uuid {entry['uuid']} — uniqueness holds " + "after every event, not just at the end" + ) + live_by_uuid[parsed] = key if problems: raise SystemExit( "uuid registry invalid:\n" @@ -1039,7 +1114,7 @@ def render_entry(entry: dict) -> str: ordered["revived"] = True elif entry.get("succeeds") is not None: ordered["succeeds"] = entry["succeeds"] - return json.dumps(ordered, ensure_ascii=False) + return json.dumps(ordered, ensure_ascii=False, allow_nan=False) def stage(self, new_entries: list[dict]) -> "UuidRegistry": """Return a new registry with entries appended and REVALIDATED. @@ -1091,7 +1166,11 @@ def build_catalog( rows whose UUID would vanish from the catalog — also gated). """ raw = observations_path.read_bytes() - rows = [json.loads(line) for line in raw.decode().splitlines() if line.strip()] + rows = [ + json.loads(line, parse_constant=_reject_json_constants) + for line in raw.decode().splitlines() + if line.strip() + ] identities = build_identities(rows) # Canonicalize each observed bucket through the existing catalog: an @@ -1182,6 +1261,17 @@ def resolve_uuid( if prior_uuid and binding and prior_uuid != binding: # The catalog row disagrees with the registry: an explicit, # gated remint (the curator edited the row's uuid on purpose). + # The replacement must be new to the registry outright. + replacement_owner = registry.uuid_owner.get( + uuid_module.UUID(prior_uuid).int + ) + if replacement_owner is not None: + raise SystemExit( + f"identity {canon_key} would supersede to uuid " + f"{prior_uuid}, which the registry already knows " + f"(first bound to {replacement_owner}) — superseding " + "UUIDs must be new" + ) # A retired binding is revived first so the event chain stays # valid (revives are staged before supersedes). if not registry.is_live(canon_key): @@ -1199,7 +1289,9 @@ def resolve_uuid( ) ) return prior_uuid - if binding: + if binding and not ( + not registry.is_live(canon_key) and canon_key in registry.consumed + ): if not registry.is_live(canon_key): # A retired identity is being observed again: same UUID, # explicit revive event (no ceremony — resuming an identity @@ -1213,6 +1305,11 @@ def resolve_uuid( ) ) return binding + if binding: + # The identity handed its lineage to a successor: terminal. + # Observations reappearing under the old key are a NEW series + # claim and mint fresh; the old UUID stays with its lineage. + prior = None row_uuid = prior_uuid if prior_uuid else str(uuid_module.uuid4()) owner_key = registry.uuid_owner.get(uuid_module.UUID(row_uuid).int) if owner_key is not None: @@ -1331,6 +1428,9 @@ def resolve_uuid( seen_docket_names: set[str] = set() for entry in docket["series"]: concept = entry["series"] + reserved = _reserved_segment_problem(concept) + if reserved: + raise SystemExit(f"docket entry: {reserved}") if concept in seen_docket_names: raise SystemExit( f"duplicate docket series id {concept!r} — docket names " @@ -1497,7 +1597,9 @@ def resolve_uuid( def render(catalog: dict) -> str: - return json.dumps(catalog, indent=2, ensure_ascii=False) + "\n" + return json.dumps( + catalog, indent=2, ensure_ascii=False, allow_nan=False + ) + "\n" def validate_uuids(catalog: dict) -> list[str]: diff --git a/tests/test_build_series_catalog.py b/tests/test_build_series_catalog.py index ae7b464..7943e10 100644 --- a/tests/test_build_series_catalog.py +++ b/tests/test_build_series_catalog.py @@ -1258,3 +1258,164 @@ def test_bare_year_variant_strips_only_for_annual_rows( "agency.annual.total.2019", {"type": "year", "value": "2025"} ) assert pattern == "agency.annual.total.2019" + + +def test_consumed_lineage_is_terminal(tmp_path: pathlib.Path) -> None: + # Fifth-review repro: retire A -> B succeeds A -> B moves to U2 -> + # retire B -> revive A restored the banned U1 excursion via detour. + path = tmp_path / "registry.jsonl" + succ = {"concept": "a.one", "geography": None, "entity": None} + events = [ + _mint("a.one", U1), + dict(_mint("a.one", U1), retired=True, note="placeholder done"), + dict(_mint("a.one", U1), succeeds=succ, + entity={"name": "economy", "role": "aggregate"}), + dict(_mint("a.one", U2, + entity={"name": "economy", "role": "aggregate"}), + supersedes=U1, note="moved along deliberately"), + dict(_mint("a.one", U2, + entity={"name": "economy", "role": "aggregate"}), + retired=True, note="successor done too"), + dict(_mint("a.one", U1), revived=True), + ] + path.write_text( + "".join(json.dumps(e) + "\n" for e in events), encoding="utf-8" + ) + with pytest.raises(SystemExit, match="terminal"): + bsc.UuidRegistry.load(path) + + +def test_builder_mints_fresh_for_consumed_identity( + tmp_path: pathlib.Path, +) -> None: + # Observations reappearing under a handed-over identity are a NEW + # series claim: fresh uuid, no revival of the terminal lineage. + succ = {"concept": "census.m3.new_orders", "geography": None, + "entity": None} + entries = [ + _mint("census.m3.new_orders", U1), + dict(_mint("census.m3.new_orders", U1), retired=True, + note="placeholder enriched"), + dict(_mint("census.m3.new_orders", U1), succeeds=succ, + geography={"level": "country", "id": "0100000US", + "vintage": "current"}, + entity={"name": "economy", "role": "aggregate"}), + ] + rows = [ + _row("census.m3.new_orders"), + _row("census.m3.new_orders", + geography=None if False else {"level": "country", "id": "GB", + "vintage": "current"}, + rid="census.m3.new_orders.gb.first_print"), + ] + catalog, plan = _build(tmp_path, rows, registry_entries=entries) + us_row = next( + r for r in catalog["series"] + if (r["geography"] or {}).get("id") == "0100000US" + ) + gb_row = next( + r for r in catalog["series"] + if (r["geography"] or {}).get("id") == "GB" + ) + assert us_row["uuid"] == U1 # the enriched successor identity + assert gb_row["uuid"] not in (U1, U2) # fresh, never the old lineage + assert len(plan["revives"]) == 0 + + +def test_live_uuid_uniqueness_holds_per_event_prefix( + tmp_path: pathlib.Path, +) -> None: + # Two live holders of one uuid mid-sequence must fail even if a later + # event would "fix" the final state. + path = tmp_path / "registry.jsonl" + events = [ + _mint("a.one", U1), + _mint("a.two", U2), + # a.two jumps onto U1 while a.one still holds it live... + dict(_mint("a.two", U1), supersedes=U2, note="deliberate theft"), + # ...and a.one is retired only afterwards. + dict(_mint("a.one", U1), retired=True, note="too late to matter"), + ] + path.write_text( + "".join(json.dumps(e) + "\n" for e in events), encoding="utf-8" + ) + with pytest.raises(SystemExit, match="after every event"): + bsc.UuidRegistry.load(path) + + +def test_reserved_placeholder_rejected_in_docket_and_registry( + tmp_path: pathlib.Path, +) -> None: + with pytest.raises(SystemExit, match="reserved placeholder"): + _build( + tmp_path, + [_row("agency.rate")], + docket={"series": [{"series": "agency.{P}.rate", + "cadence": "monthly"}]}, + ) + path = tmp_path / "registry.jsonl" + path.write_text( + json.dumps(_mint("agency.{P}.rate", U1)) + "\n", encoding="utf-8" + ) + with pytest.raises(SystemExit, match="reserved placeholder"): + bsc.UuidRegistry.load(path) + + +def test_registry_rejects_json_constants_and_empty_dimensions( + tmp_path: pathlib.Path, +) -> None: + path = tmp_path / "registry.jsonl" + nan_line = ( + '{"concept": "a.one", "geography": {"level": "country", ' + f'"id": "US", "vintage": NaN}}, "entity": null, "uuid": "{U1}"}}' + ) + path.write_text(nan_line + "\n", encoding="utf-8") + with pytest.raises(SystemExit, match="not strict JSON"): + bsc.UuidRegistry.load(path) + empty_vintage = _mint( + "a.one", U1, + geography={"level": "country", "id": "US", "vintage": ""}, + ) + path.write_text(json.dumps(empty_vintage) + "\n", encoding="utf-8") + with pytest.raises(SystemExit, match="nonempty string or null"): + bsc.UuidRegistry.load(path) + + +def test_observation_dimension_domains_are_enforced( + tmp_path: pathlib.Path, +) -> None: + bad = _row("agency.rate") + bad["geography"] = {"level": "country", "id": "0100000US", "vintage": ""} + with pytest.raises(SystemExit, match="nonempty string or null"): + _build(tmp_path, [bad]) + + +@pytest.mark.parametrize( + ("identifier", "period"), + [ + ("agency.rate.january_1899", MONTH_2026_06), + ("agency.rate.january_3000", MONTH_2026_06), + ], +) +def test_boundary_year_month_names_neither_strip_nor_crash( + identifier: str, period: dict +) -> None: + assert bsc.family_pattern(identifier, period) == identifier + + +@pytest.mark.parametrize("value", ["1899-01", "3000-01"]) +def test_out_of_window_quarter_periods_are_malformed( + tmp_path: pathlib.Path, value: str +) -> None: + with pytest.raises(SystemExit, match="malformed period"): + _build( + tmp_path, + [_row("agency.rate", period={"type": "quarter", "value": value})], + ) + + +@pytest.mark.parametrize("segment", ["1899_13", "2999_13", "3000_13"]) +def test_out_of_window_impossible_tokens_are_flagged(segment: str) -> None: + pattern = bsc.family_pattern(f"agency.rate.{segment}", MONTH_2026_06) + assert pattern == f"agency.rate.{segment}" + assert bsc.suspect_segments(pattern) == [segment] From 33974add1071fde2e49a7c2a4f6c07c4cd18be5d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 2 Aug 2026 13:52:06 -0400 Subject: [PATCH 11/11] Answer sixth adversarial review: source-bound alias, reclaim events, strict inputs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All five findings addressed: 1. [HIGH] The StatCan docket alias moves to the lineage its own sourceBinding names: the seed binds v64549350, whose observations live on the ei_beneficiary row, so the curated alias now sits there (catalog-only change; no UUIDs move; 46 docket-only stable). 2. [HIGH] A handed-over key that re-forms takes an explicit 'reclaimed' mint: fresh UUID, substantive note, valid only where the predecessor was consumed by a succeeds event — the previous markerless mint died in the writer's own stage revalidation. 3. [HIGH] Strictness is uniform: docket seeds and observation rows parse with duplicate-member rejection and NaN/Infinity refusal; observation dimensions are validated RAW (""/[] fail loudly instead of collapsing to null); succeeds objects are schema-checked (identity fields only, nested dimension domains). 4. [MEDIUM] Registry lines split on physical LF only, so vertical-tab or U+2028 separators can no longer hide extra events on one line. 5. [MEDIUM] The new-branch fallback fetch is fully qualified (refs/heads/...), immune to same-named tag shadowing. 113 tests; ruff, doctest, --check, byte-idempotence green; identity anchor unchanged. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 5 +- ledger/series_catalog.json | 2 +- scripts/build_series_catalog.py | 88 ++++++++++-- tests/test_build_series_catalog.py | 206 +++++++++++++++++++++++++++++ 4 files changed, 290 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7957c9b..23b7556 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,7 +64,10 @@ jobs: FALLBACK_REF: codex/thesis-ledger-facts run: | if [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then - git fetch --no-tags --depth=1 origin "$FALLBACK_REF" + # Fully qualified so a same-named tag can never shadow the + # protected branch. + git fetch --no-tags --depth=1 origin \ + "refs/heads/$FALLBACK_REF" BASE_SHA=FETCH_HEAD else git fetch --no-tags --depth=1 origin "$BASE_SHA" diff --git a/ledger/series_catalog.json b/ledger/series_catalog.json index 6955c2d..a6f2581 100644 --- a/ledger/series_catalog.json +++ b/ledger/series_catalog.json @@ -5622,6 +5622,7 @@ "statcan" ], "aliases": [ + "statcan.employment_insurance.regular_beneficiaries", "statcan.employment_insurance.regular_beneficiaries.canada.may_2026" ], "source_concepts": [ @@ -5657,7 +5658,6 @@ "statcan" ], "aliases": [ - "statcan.employment_insurance.regular_beneficiaries", "statcan.employment_insurance.regular_beneficiaries.canada.april_2026" ], "source_concepts": [ diff --git a/scripts/build_series_catalog.py b/scripts/build_series_catalog.py index 9436cc9..781e9a9 100644 --- a/scripts/build_series_catalog.py +++ b/scripts/build_series_catalog.py @@ -556,7 +556,12 @@ def build_identities(rows: list[dict]) -> dict[tuple[str, str, str], dict]: ("geography", ("level", "id", "vintage", "name")), ("entity", ("name", "role")), ): - domain = _dimension_problem(row.get(what) or None, allowed, what) + raw_dim = row.get(what) + if raw_dim in (None, {}): + continue + # Validate the RAW value: "" or [] must fail loudly, never + # collapse into the null identity. + domain = _dimension_problem(raw_dim, allowed, what) if domain: raise SystemExit(f"observation row {index}: {domain}") if ( @@ -819,7 +824,10 @@ def __init__(self, path: pathlib.Path, raw: bytes) -> None: problems.append("registry must be LF-only (CR byte found)") if raw and not raw.endswith(b"\n"): problems.append("registry must end with a newline") - for lineno, line in enumerate(raw.decode("utf-8").splitlines(), start=1): + physical_lines = raw.decode("utf-8").split("\n") + if physical_lines and physical_lines[-1] == "": + physical_lines.pop() + for lineno, line in enumerate(physical_lines, start=1): if not line.strip(): problems.append(f"line {lineno}: blank line") continue @@ -864,8 +872,11 @@ def __init__(self, path: pathlib.Path, raw: bytes) -> None: retired = entry.get("retired") revived = entry.get("revived") succeeds = entry.get("succeeds") + reclaimed = entry.get("reclaimed") markers = sum( - 1 for marker in (supersedes, retired, revived, succeeds) + 1 + for marker in (supersedes, retired, revived, succeeds, + reclaimed) if marker is not None ) if markers > 1: @@ -891,8 +902,29 @@ def __init__(self, path: pathlib.Path, raw: bytes) -> None: elif previous is None: problems.append( f"line {lineno}: {key} has no prior binding to " - "supersede/retire/revive" + "supersede/retire/revive/reclaim" ) + elif reclaimed is not None: + if reclaimed is not True: + problems.append(f"line {lineno}: reclaimed must be true") + if not (self._is_retired(previous) and key in self.consumed): + problems.append( + f"line {lineno}: {key} reclaims a lineage that was " + "not handed over — reclaim exists only for keys " + "whose predecessor was consumed by a succeeds event" + ) + if parsed in self.uuid_owner: + problems.append( + f"line {lineno}: reclaim for {key} reuses uuid " + f"{entry['uuid']} (first bound to " + f"{self.uuid_owner[parsed]}) — a reclaimed identity " + "is a NEW series and mints fresh" + ) + if not _usable_note(entry.get("note")): + problems.append( + f"line {lineno}: reclaim for {key} requires a " + "substantive note" + ) elif supersedes is not None: if self._is_retired(previous): problems.append( @@ -997,6 +1029,21 @@ def _succeeds_problem( ) -> str | None: if not isinstance(succeeds, dict): return f"line {lineno}: succeeds must be an identity object" + unknown = set(succeeds) - {"concept", "geography", "entity"} + if unknown: + return ( + f"line {lineno}: succeeds has non-identity fields " + f"{sorted(unknown)}" + ) + for what, allowed in ( + ("geography", ("level", "id", "vintage")), + ("entity", ("name", "role")), + ): + domain = _dimension_problem( + succeeds.get(what), allowed, f"succeeds.{what}" + ) + if domain: + return f"line {lineno}: {domain}" predecessor_key = self.entry_key(succeeds) predecessor = self.latest.get(predecessor_key) if predecessor is None: @@ -1114,6 +1161,9 @@ def render_entry(entry: dict) -> str: ordered["revived"] = True elif entry.get("succeeds") is not None: ordered["succeeds"] = entry["succeeds"] + elif entry.get("reclaimed") is not None: + ordered["reclaimed"] = True + ordered["note"] = entry["note"] return json.dumps(ordered, ensure_ascii=False, allow_nan=False) def stage(self, new_entries: list[dict]) -> "UuidRegistry": @@ -1167,7 +1217,11 @@ def build_catalog( """ raw = observations_path.read_bytes() rows = [ - json.loads(line, parse_constant=_reject_json_constants) + json.loads( + line, + object_pairs_hook=_reject_duplicate_keys, + parse_constant=_reject_json_constants, + ) for line in raw.decode().splitlines() if line.strip() ] @@ -1307,9 +1361,21 @@ def resolve_uuid( return binding if binding: # The identity handed its lineage to a successor: terminal. - # Observations reappearing under the old key are a NEW series - # claim and mint fresh; the old UUID stays with its lineage. - prior = None + # Anything reappearing under the old key is a NEW series claim + # and mints fresh through an explicit reclaim event; the old + # UUID stays with its lineage. + row_uuid = str(uuid_module.uuid4()) + plan["mints"].append( + dict( + _registry_event(canon_key[0], geography, entity, row_uuid), + reclaimed=True, + note=( + "identity re-established after its lineage was " + "handed over" + ), + ) + ) + return row_uuid row_uuid = prior_uuid if prior_uuid else str(uuid_module.uuid4()) owner_key = registry.uuid_owner.get(uuid_module.UUID(row_uuid).int) if owner_key is not None: @@ -1417,7 +1483,11 @@ def resolve_uuid( docket_raw = b"" if docket_path is not None: docket_raw = docket_path.read_bytes() - docket = json.loads(docket_raw.decode()) + docket = json.loads( + docket_raw.decode(), + object_pairs_hook=_reject_duplicate_keys, + parse_constant=_reject_json_constants, + ) alias_tally = Counter(alias for row in series for alias in row["aliases"]) name_rows: dict[str, list[dict]] = {} for row in series: diff --git a/tests/test_build_series_catalog.py b/tests/test_build_series_catalog.py index 7943e10..c64a486 100644 --- a/tests/test_build_series_catalog.py +++ b/tests/test_build_series_catalog.py @@ -1419,3 +1419,209 @@ def test_out_of_window_impossible_tokens_are_flagged(segment: str) -> None: pattern = bsc.family_pattern(f"agency.rate.{segment}", MONTH_2026_06) assert pattern == f"agency.rate.{segment}" assert bsc.suspect_segments(pattern) == [segment] + + +def test_reclaimed_mint_grammar(tmp_path: pathlib.Path) -> None: + path = tmp_path / "registry.jsonl" + succ = {"concept": "a.one", "geography": None, "entity": None} + base = [ + _mint("a.one", U1), + dict(_mint("a.one", U1), retired=True, note="placeholder done"), + dict(_mint("a.one", U1), succeeds=succ, + entity={"name": "economy", "role": "aggregate"}), + ] + fresh = "dddddddd-4444-4444-8444-444444444444" + good = dict(_mint("a.one", fresh), reclaimed=True, + note="re-established after handover") + path.write_text( + "".join(json.dumps(e) + "\n" for e in base + [good]), + encoding="utf-8", + ) + registry = bsc.UuidRegistry.load(path) + assert registry.binding(("a.one", bsc._geo_key(None), + bsc._entity_key(None))) == fresh + # Reclaim of a merely-retired (not consumed) key is invalid. + unconsumed = [ + _mint("b.two", U2), + dict(_mint("b.two", U2), retired=True, note="ordinary retirement"), + dict(_mint("b.two", fresh), reclaimed=True, + note="not a handover case"), + ] + path.write_text( + "".join(json.dumps(e) + "\n" for e in unconsumed), encoding="utf-8" + ) + with pytest.raises(SystemExit, match="not handed over"): + bsc.UuidRegistry.load(path) + # Reclaim must mint fresh, never reuse. + reuse = dict(_mint("a.one", U1), reclaimed=True, + note="tries to take U1 back") + path.write_text( + "".join(json.dumps(e) + "\n" for e in base + [reuse]), + encoding="utf-8", + ) + with pytest.raises(SystemExit, match="mints fresh"): + bsc.UuidRegistry.load(path) + + +def test_consumed_key_reclaims_fresh_lineage(tmp_path: pathlib.Path) -> None: + # Sixth-review repro: a docket entry re-forming a handed-over key must + # take an explicit reclaim path (previously it staged a markerless + # mint that its own validator rejected). + succ = {"concept": "census.m3.new_orders", "geography": None, + "entity": None} + entries = [ + _mint("census.m3.new_orders", U1), + dict(_mint("census.m3.new_orders", U1), retired=True, + note="placeholder enriched"), + dict(_mint("census.m3.new_orders", U1), succeeds=succ, + geography={"level": "country", "id": "0100000US", + "vintage": "current"}, + entity={"name": "economy", "role": "aggregate"}), + _mint("bls.cps.unemployment_rate", U2, + geography={"level": "country", "id": "0100000US", + "vintage": "current"}, + entity={"name": "economy", "role": "aggregate"}), + ] + existing = { + "series": [ + { + "uuid": U2, + "concept": "bls.cps.unemployment_rate", + "geography": dict(US), + "entity": {"name": "economy", "role": "aggregate"}, + "aliases": [], + "status": "observed", + } + ] + } + observations = tmp_path / "obs.jsonl" + observations.write_text( + json.dumps(_row("bls.cps.unemployment_rate")) + "\n", + encoding="utf-8", + ) + docket_path = tmp_path / "seed.json" + docket_path.write_text( + json.dumps({"series": [{"series": "census.m3.new_orders", + "cadence": "monthly"}]}), + encoding="utf-8", + ) + catalog_path = tmp_path / "catalog.json" + catalog_path.write_text(json.dumps(existing) + "\n", encoding="utf-8") + registry = _registry(tmp_path, entries) + catalog, plan = bsc.build_catalog( + observations, docket_path, bsc.ExistingCatalog(catalog_path), registry + ) + docket_row = next( + r for r in catalog["series"] if r["status"] == "docket-only" + ) + assert docket_row["uuid"] != U1 # fresh lineage, old uuid stays put + reclaim = next(e for e in plan["mints"] if e.get("reclaimed")) + assert reclaim["uuid"] == docket_row["uuid"] + # The enriched identity has no row in this synthetic catalog, so its + # live binding is a gated retire — and the staged whole must reload. + staged = registry.stage( + plan["enrich_retires"] + plan["mints"] + plan["revives"] + + plan["supersedes"] + + [dict(e, note="synthetic gate approval") + for e in plan["retire_pending"]] + ) + assert staged.entries[-1] is not None + + +def test_docket_seed_rejects_json_constants(tmp_path: pathlib.Path) -> None: + argv = _repo(tmp_path, [_row("bls.cps.unemployment_rate")]) + (tmp_path / "seed.json").write_text( + '{"series": [{"series": "a.b", "cadence": "monthly", ' + '"extras": {"valueScale": NaN}}]}', + encoding="utf-8", + ) + with pytest.raises(ValueError, match="not allowed"): + bsc.main(argv) + + +def test_observation_duplicate_members_rejected( + tmp_path: pathlib.Path, +) -> None: + row = _row("agency.rate") + line = json.dumps(row) + line = line.replace( + '"geography": {', '"geography": {"level": "state", ', 1 + ) + observations = tmp_path / "obs.jsonl" + observations.write_text( + line.replace('"geography": {"level": "state", "level"', + '"geography": {"level": "state", "level"') + "\n", + encoding="utf-8", + ) + registry = _registry(tmp_path) + with pytest.raises(ValueError, match="duplicate JSON member"): + bsc.build_catalog( + observations, None, + bsc.ExistingCatalog(tmp_path / "catalog.json"), registry, + ) + + +@pytest.mark.parametrize( + ("field", "value"), + [("geography", ""), ("geography", []), ("entity", []), ("entity", "x")], +) +def test_falsey_or_nonobject_dimensions_rejected( + tmp_path: pathlib.Path, field: str, value +) -> None: + bad = _row("agency.rate") + bad[field] = value + with pytest.raises(SystemExit, match="must be an object or null"): + _build(tmp_path, [bad]) + + +def test_succeeds_schema_is_enforced(tmp_path: pathlib.Path) -> None: + path = tmp_path / "registry.jsonl" + base = [ + _mint("a.one", U1), + dict(_mint("a.one", U1), retired=True, note="placeholder done"), + ] + forged = dict( + _mint("a.one", U1, entity={"name": "economy", "role": "aggregate"}), + succeeds={"concept": "a.one", "geography": None, "entity": None, + "extra": "field"}, + ) + path.write_text( + "".join(json.dumps(e) + "\n" for e in base + [forged]), + encoding="utf-8", + ) + with pytest.raises(SystemExit, match="non-identity fields"): + bsc.UuidRegistry.load(path) + bad_geo = dict( + _mint("a.one", U1, entity={"name": "economy", "role": "aggregate"}), + succeeds={"concept": "a.one", + "geography": {"level": "country", "id": ""}, + "entity": None}, + ) + path.write_text( + "".join(json.dumps(e) + "\n" for e in base + [bad_geo]), + encoding="utf-8", + ) + with pytest.raises(SystemExit, match="nonempty string or null"): + bsc.UuidRegistry.load(path) + + +def test_registry_rejects_hidden_line_separators( + tmp_path: pathlib.Path, +) -> None: + path = tmp_path / "registry.jsonl" + two_on_one = ( + json.dumps(_mint("a.one", U1)) + + "
" + + json.dumps(_mint("a.two", U2)) + ) + path.write_text(two_on_one + "\n", encoding="utf-8") + with pytest.raises(SystemExit, match="not strict JSON"): + bsc.UuidRegistry.load(path) + with_vt = ( + json.dumps(_mint("a.one", U1)) + + "\x0b" + + json.dumps(_mint("a.two", U2)) + ) + path.write_bytes(with_vt.encode("utf-8") + b"\n") + with pytest.raises(SystemExit, match="not strict JSON"): + bsc.UuidRegistry.load(path)