Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -190,27 +190,39 @@ jobs:

assert policyengine_chronicle.__name__ == "policyengine_chronicle"
# Every consumer-fact contract must ship inside the wheel: frozen v1,
# the v2 labels contract, the v3 geography-name contract, and the v4
# contract rows are emitted under.
# the v2 labels contract, the v3 geography-name contract, the v4
# geography-register contract, and the v5 contract rows are emitted
# under.
assert (
consumer_fact_schema("ledger.consumer_fact.v1")["title"]
== "Ledger consumer fact contract row"
)
assert consumer_fact_schema()["title"] == "Chronicle consumer fact contract row"
assert consumer_fact_schema()["properties"]["schema_version"] == {
"const": "chronicle.consumer_fact.v4"
"const": "chronicle.consumer_fact.v5"
}
assert set(CONSUMER_FACT_SCHEMA_SHA256_BY_VERSION) == {
"ledger.consumer_fact.v1",
"chronicle.consumer_fact.v2",
"chronicle.consumer_fact.v3",
"chronicle.consumer_fact.v4",
"chronicle.consumer_fact.v5",
}
# v4 rows take their UK area names from the register, which the
# export reads from package data, so the wheel must carry it.
from chronicle.geography_names import canonical_geography_name

assert canonical_geography_name("E06000001") == "Hartlepool"
# v5 rows take shared dimension value labels from the register, which
# facts read from package data at build time.
from chronicle.dimension_labels import canonical_value_label

assert (
canonical_value_label(
"us:statutes/26/62#adjusted_gross_income", "under_1"
)
== "No adjusted gross income"
)
assert len(CONSUMER_FACT_SCHEMA_SHA256) == 64
PY
/tmp/chronicle-wheel-smoke/bin/chronicle --help >/dev/null
Expand Down
32 changes: 22 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,9 +289,17 @@ carry canonical constraints explicitly in `universe_constraints`; source-layout
`dimensions` are metadata and are not target constraints. Rows also carry
Chronicle-owned labels for every dimension and dimension value
(`dimension_labels`, `dimension_value_labels`, `layout.groupby_dimension_label`;
chronicle#261), which Microcosm's calibration hierarchy displays. Every UK
package must label all of them, and `build-bundle` reports a gap as an error;
see `docs/agent-source-package-harness.md` for where labels come from.
chronicle#261), which Microcosm's calibration hierarchy displays. Every package
must label all of them, and `build-bundle` reports a gap as an error
(chronicle#265). One dimension value carries one label (chronicle#376): a value
two or more packages emit takes the label in
`chronicle/dimension_values/canonical_labels.json` at every build year, and the
package's own wording, where it differs, rides in
`package_dimension_value_labels` (the package's text, which is not checked
against the publisher's file and is often Chronicle's). A value labelled two ways is an error, never
a warning: `validate-package` and `build-suite` refuse one a package's rows label
two ways, and `build-bundle` also refuses one two packages label differently.
See `docs/agent-source-package-harness.md` for where labels come from.

For the UK source-package feed, build the curated UK suite and then a facts-only
consumer artifact:
Expand Down Expand Up @@ -580,15 +588,19 @@ payload as its Ledger key; only the prefix differs.
- **The row contract moves on its own.** Each contract in
`CONSUMER_FACT_ROW_CONTRACTS` adds to the one before it, so a row restamped
with a later id stays valid: `chronicle.consumer_fact.v2` (chronicle#261) is
the frozen v1 row plus optional dimension and value labels, and
the frozen v1 row plus optional dimension and value labels,
`chronicle.consumer_fact.v3` (chronicle#266) is v2 plus the publisher's name
for the fact's geography. Rows are emitted as v3 with Ledger-named keys, and
each row validates against the schema of the contract it names. Only v1 and
v2 name a naming epoch: the epoch table renames identifiers, it does not
version the row.
for the fact's geography, `chronicle.consumer_fact.v4` (chronicle#281) is v3
with one name per UK geography identifier and the publisher's text in an
optional `geography.publisher_name`, and `chronicle.consumer_fact.v5`
(chronicle#376) is v4 with one label per dimension value and the package's
own wording in an optional `package_dimension_value_labels`. Rows are
emitted as v5 with Ledger-named keys, and each row validates against the
schema of the contract it names. Only v1 and v2 name a naming epoch: the
epoch table renames identifiers, it does not version the row.
- **Artifacts canonicalize on emit.** The consumer artifact pins the sha256 of
the v3 consumer-fact schema, so `build_consumer_artifact` rewrites every row
it read to Ledger-named keys and the v3 contract before writing it; an
the v5 consumer-fact schema, so `build_consumer_artifact` rewrites every row
it read to Ledger-named keys and the v5 contract before writing it; an
artifact built from mixed-epoch rows is byte-identical to one built from the
same rows written Ledger-named. Loads accept an artifact pinned to any
packaged schema whose rows use that contract. Asking an artifact boundary to
Expand Down
1 change: 1 addition & 0 deletions changelog.d/376.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
One dimension value now carries one label, whichever packages and build years a consumer combines. Every value that two or more packages emit, or that its packages word more than one way, has one label in Chronicle's register, `chronicle/dimension_values/canonical_labels.json` (4,824 values). Facts take that label in `dimension_value_labels` and `layout.groupby_value_label` when they are built, and keep the package's own wording, where it differs, in the new optional `package_dimension_value_labels`, which is the package's text (not checked against the publisher's file, and often Chronicle's own); rows are emitted as `chronicle.consumer_fact.v5`, v4 plus that field. The IRS filing-season week-47 rows for adjusted gross income `under_1` keep the IRS header "No adjusted gross income and deficit" as their label and in that field, and their facts carry "No adjusted gross income", as every other IRS SOI package does. A value labelled two ways is an error, never a warning: `validate-package` and `build-suite` refuse one that a package labels two ways, and `build-bundle` also refuses one that two packages label differently; the list of accepted label variants is gone. A package may not declare a label the register contradicts, and `scripts/build_dimension_value_register.py` regenerates the register, with a `--static-check` that compares it with the package files in CI. No fact key or value moves (chronicle#376).
46 changes: 27 additions & 19 deletions chronicle/bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
from pathlib import Path
from typing import Any, Callable, Sequence

from chronicle.dimension_labels import dimension_label_issues, dimension_labels_by_id
from chronicle.dimension_labels import (
cross_package_value_label_remedy,
dimension_label_issues,
dimension_labels_by_id,
)
from chronicle.epoch import canonicalize_key, schema_id
from chronicle.source_package import (
ARTIFACT_YEAR_RESTAMP_CODE,
Expand Down Expand Up @@ -682,8 +686,8 @@ def build_bundle(
).append(source)

errors.extend(_cross_package_dimension_label_errors(dimension_labels))
errors.extend(_cross_package_value_label_errors(value_labels))
warnings.extend(_cross_package_geography_name_warnings(geography_names))
warnings.extend(_cross_package_value_label_warnings(value_labels))
aggregate_duplicates = _duplicate_key_reports(
consumer_rows,
"aggregate_fact_key",
Expand Down Expand Up @@ -764,13 +768,15 @@ def _dimension_label_reports(
source: str,
rows: list[dict[str, Any]],
) -> tuple[list[BuildBundleIssue], list[BuildBundleIssue]]:
"""Report one source's dimension-label issues (chronicle#261, #265).
"""Report one source's dimension-label issues (chronicle#261, #265, #376).

Every package must label every dimension and value it emits, whichever
country it publishes for: the row contract is one contract, and a consumer
reads the labels the same way everywhere. The exception is publisher row
labels that differ across the rows of one groupby value: Chronicle keeps
that text as published and warns.
reads the labels the same way everywhere. A value one package labels two
ways, the package's row labels (its record-set rows' ``label:``) that
differ across one groupby value's rows included, is an error too: a
consumer would see one value under two names (chronicle#376). Nothing
about labels is only a warning.
"""
errors: list[BuildBundleIssue] = []
warnings: list[BuildBundleIssue] = []
Expand All @@ -785,10 +791,7 @@ def _dimension_label_reports(
else f"{issue.dimension_id}={issue.value_id}"
),
)
if issue.code == "conflicting_groupby_value_label":
warnings.append(report)
else:
errors.append(report)
errors.append(report)
return errors, warnings


Expand Down Expand Up @@ -903,16 +906,21 @@ def _value_labels(rows: list[dict[str, Any]]) -> dict[tuple[str, str], set[str]]
return found


def _cross_package_value_label_warnings(
def _cross_package_value_label_errors(
labels_by_value: dict[tuple[str, str], dict[str, list[str]]],
) -> list[BuildBundleIssue]:
"""Report a dimension value two packages label differently (chronicle#265).

One label per dimension id is an error because a consumer selects on the
id; one label per value is a warning because the wording belongs to the
table the value was read from, and two tables of one publisher may word a
band or a total differently. A consumer target that spans both sees both,
so the bundle says where that is possible.
"""Refuse a dimension value two packages label differently (chronicle#376).

Microcosm copies each fact's labels into every target it compiles, so a
release built from both packages would label one value two ways, and a
consumer that groups targets by value cannot show it as one (the IRS
filing-season week-47 table labelled adjusted gross income ``under_1`` "No
adjusted gross income and deficit" where every other IRS SOI package writes
"No adjusted gross income"). Every fact takes the label Chronicle's
register gives a value it lists, so this finds values the register does not
list yet. There are no accepted exceptions: two packages emit the value,
so the one remedy is a register entry, and the message says how to choose
its label (or names the register's label, for rows built before it).
"""
return [
BuildBundleIssue(
Expand All @@ -924,7 +932,7 @@ def _cross_package_value_label_warnings(
f"{label!r} in {sorted(sources)}"
for label, sources in sorted(labels.items())
)
+ "."
+ f". {cross_package_value_label_remedy(dimension_id, value_id)}"
),
key=f"{dimension_id}={value_id}",
)
Expand Down
8 changes: 8 additions & 0 deletions chronicle/consumer_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,14 @@ def _consumer_fact_row(
dimension_id: dict(value_labels)
for dimension_id, value_labels in fact.dimension_value_labels.items()
},
# The package's own wording where Chronicle's register relabels a value
# (chronicle#376); display metadata, like the labels, never in a key.
"package_dimension_value_labels": {
dimension_id: dict(value_labels)
for dimension_id, value_labels in (
fact.package_dimension_value_labels.items()
)
},
"universe_constraints": _universe_constraint_set_payload(fact),
"source": _clean(fact_to_mapping(fact)["source"]),
"lineage": {
Expand Down
7 changes: 7 additions & 0 deletions chronicle/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,13 @@ class AggregateFact:
# keyed by ``consumer_contract.dimension_value_id``.
dimension_labels: dict[str, str] = field(default_factory=dict)
dimension_value_labels: dict[str, dict[str, str]] = field(default_factory=dict)
# The package's own wording of a value, keyed like dimension_value_labels,
# where it differs from the label Chronicle's register gives the value
# (chronicle#376). Display metadata too, and not checked against the
# publisher's file: often it is text Chronicle wrote.
package_dimension_value_labels: dict[str, dict[str, str]] = field(
default_factory=dict
)


@dataclass(frozen=True)
Expand Down
11 changes: 8 additions & 3 deletions chronicle/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -1058,9 +1058,14 @@ def _build_id(
def _canonical_fact_mapping(fact: AggregateFact) -> dict[str, Any]:
"""Return a build-hash payload independent of accepted key epochs."""
mapping = asdict(fact)
# Labels (chronicle#261) enter the hash only when present, so a fact
# without them keeps the build hash it had before they existed.
for label_field in ("dimension_labels", "dimension_value_labels"):
# Labels (chronicle#261) and the package's own value wording (chronicle#376)
# enter the hash only when present, so a fact without them keeps the build
# hash it had before they existed.
for label_field in (
"dimension_labels",
"dimension_value_labels",
"package_dimension_value_labels",
):
if not mapping[label_field]:
mapping.pop(label_field)
if mapping["layout"] and mapping["layout"]["groupby_dimension_label"] is None:
Expand Down
Loading
Loading