Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f68b63a
Add execution receipt schema v1 with RFC 8785 digests
MaxGhenis Oct 11, 2026
2d4cb9a
Fix review findings: strict digests, bindable modifiers, core in cert…
MaxGhenis Oct 11, 2026
7df93cf
Fix round-2 review findings on execution receipt v1
MaxGhenis Oct 11, 2026
5ec9bbf
Refuse module code that did not run; close round-3 test gaps
MaxGhenis Oct 11, 2026
402900a
Type the module-source compile helpers
MaxGhenis Oct 11, 2026
fe3bd83
Fix round-4 review findings on execution receipt v1
MaxGhenis Oct 12, 2026
8267786
Record the years the dataset is projected to; flag years outside it
MaxGhenis Oct 12, 2026
478a1e9
Fix round-5 review findings on execution receipt v1
MaxGhenis Oct 12, 2026
77277eb
Refuse modifiers that read module state the digest cannot bind
MaxGhenis Oct 12, 2026
6726fd1
Fix round-6 review findings on execution receipt v1
MaxGhenis Oct 12, 2026
ac3b3fa
Bind a modifier's module by shape; compare constants with their sharing
MaxGhenis Oct 12, 2026
801283a
Fix round-8 review findings: introspection, metadata and Python 3.14
MaxGhenis Oct 12, 2026
cf809bd
Fix round-9 review findings: version drift, annotation readers, generics
MaxGhenis Oct 13, 2026
bbb93a6
Fix receipt build snapshots, install provenance and annotation checks
MaxGhenis Oct 14, 2026
48b20bf
Bind held annotations to their source and loaded code to its distribu…
MaxGhenis Oct 14, 2026
543f57e
Hold receipt tests to how the repository happens to be installed
MaxGhenis Oct 14, 2026
5756231
Compare annotations in order and local installs by their install mode
MaxGhenis Oct 14, 2026
b7a262e
Refuse editable installs and unbound generic type parameters in receipts
MaxGhenis Oct 15, 2026
603ea1f
Refuse typing aliases built with a union inside measure functions
MaxGhenis Oct 15, 2026
f5bc1b8
Resolve function-code type names conservatively and inspect forward r…
MaxGhenis Oct 15, 2026
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
1 change: 1 addition & 0 deletions changelog.d/570.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add execution receipt schema v1 (`policyengine.provenance.receipt`), which records exactly what produced a UK result. It records the installed wrapper, model and core versions, with wheel hashes only when the builder verified them against the wheel RECORD, plus the data release and the SHA-256 of the simulated dataset bytes. The certificate digest is attached only when the certificate covers that model, core, dataset and wrapper. The receipt also carries reform and baseline digests, which refuse modifiers they cannot bind, and the fiscal-year convention. Digests use RFC 8785 canonical JSON over strict JSON values (`policyengine.provenance.canonical`), and the JSON Schema ships at `policyengine/data/schemas/execution_receipt.v1.schema.json`.
1 change: 1 addition & 0 deletions docs/_quarto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ website:
- reference/index.md
- countries.md
- release-bundles.md
- receipts.md
- data-publishing-design.md
- examples.md
- dev.md
Expand Down
164 changes: 164 additions & 0 deletions docs/receipts.md

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ classifiers = [
"Programming Language :: Python :: 3.14",
]
dependencies = [
"pydantic>=2.0.0",
"pydantic>=2.10.0",
"pandas>=2.0.0",
"h5py>=3.0.0",
"microdf_python>=1.3.0,<1.4",
Expand Down Expand Up @@ -69,6 +69,7 @@ dev = [
"mypy>=1.11.0",
"pytest-cov>=5.0.0",
"hypothesis>=6.100.0",
"rfc8785>=0.1.4",
"policyengine-core==3.33.0",
"policyengine-us==2.2.1",
"policyengine-uk==2.125.1",
Expand Down Expand Up @@ -96,6 +97,9 @@ testpaths = [
filterwarnings = [
"ignore::pydantic.warnings.PydanticDeprecatedSince20",
]
markers = [
"real_install: runs the receipt's loaded-package check against the real installation",
]

[tool.ruff]
line-length = 88
Expand Down
60 changes: 60 additions & 0 deletions scripts/generate_receipt_schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Write the published JSON Schemas for policyengine.py receipt contracts.

The Pydantic models in ``policyengine.provenance.receipt`` are the source;
the schema files under ``src/policyengine/data/schemas/`` are what other
languages consume. ``tests/test_execution_receipt.py`` checks that the two
accept and reject the same receipts.

Usage::

python scripts/generate_receipt_schemas.py # write
python scripts/generate_receipt_schemas.py --check # fail if stale
"""

from __future__ import annotations

import argparse
import json
import os
import sys
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parents[1]
SCHEMA_DIR = REPO_ROOT / "src" / "policyengine" / "data" / "schemas"
os.environ.setdefault("POLICYENGINE_SKIP_COUNTRY_IMPORTS", "1")
sys.path.insert(0, str(REPO_ROOT / "src"))

from policyengine.provenance.receipt import ExecutionReceipt # noqa: E402

SCHEMAS = {
"execution_receipt.v1.schema.json": ExecutionReceipt.json_schema,
}


def render(builder) -> str:
return json.dumps(builder(), indent=2, sort_keys=True, ensure_ascii=False) + "\n"


def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--check", action="store_true")
args = parser.parse_args(argv)
stale = []
for filename, builder in SCHEMAS.items():
path = SCHEMA_DIR / filename
content = render(builder)
if path.exists() and path.read_text() == content:
continue
if args.check:
stale.append(path)
continue
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content)
print(f"wrote {path.relative_to(REPO_ROOT)}")
for path in stale:
print(f"{path.relative_to(REPO_ROOT)} is not up to date.", file=sys.stderr)
return 1 if stale else 0


if __name__ == "__main__":
raise SystemExit(main())
8 changes: 8 additions & 0 deletions src/policyengine/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,11 @@
uk = None # type: ignore[assignment]

__all__ = ["Simulation", "outputs", "uk", "us"]

# The wrapper's own version as imported, for receipts to compare with what is
# installed when they are built.
from policyengine.provenance.loaded import ( # noqa: E402
record_loaded_versions as _record_loaded_versions,
)

_record_loaded_versions(("policyengine",))
Loading
Loading