From a63ae03ba51f23e5f82cb6ab07f93c33033dc19e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 13 Apr 2026 13:21:04 -0400 Subject: [PATCH 1/4] Add TRACE TRO export for certified bundles --- changelog.d/codex-trace-tro-export.changed.md | 1 + src/policyengine/core/__init__.py | 7 + .../core/tax_benefit_model_version.py | 24 +- src/policyengine/core/trace_tro.py | 258 ++++++++++++++++++ tests/test_release_manifests.py | 189 +++++++++++++ 5 files changed, 478 insertions(+), 1 deletion(-) create mode 100644 changelog.d/codex-trace-tro-export.changed.md create mode 100644 src/policyengine/core/trace_tro.py diff --git a/changelog.d/codex-trace-tro-export.changed.md b/changelog.d/codex-trace-tro-export.changed.md new file mode 100644 index 00000000..d3c4670b --- /dev/null +++ b/changelog.d/codex-trace-tro-export.changed.md @@ -0,0 +1 @@ +Add TRACE TRO export helpers for certified runtime bundles and expose them through `policyengine.core`. diff --git a/src/policyengine/core/__init__.py b/src/policyengine/core/__init__.py index bc3c0d27..16021ca1 100644 --- a/src/policyengine/core/__init__.py +++ b/src/policyengine/core/__init__.py @@ -36,6 +36,13 @@ from .tax_benefit_model_version import ( TaxBenefitModelVersion as TaxBenefitModelVersion, ) +from .trace_tro import ( + build_trace_tro_from_release_bundle as build_trace_tro_from_release_bundle, +) +from .trace_tro import ( + compute_trace_composition_fingerprint as compute_trace_composition_fingerprint, +) +from .trace_tro import serialize_trace_tro as serialize_trace_tro from .variable import Variable as Variable # Rebuild models to resolve forward references diff --git a/src/policyengine/core/tax_benefit_model_version.py b/src/policyengine/core/tax_benefit_model_version.py index f253fc5c..c24dfee6 100644 --- a/src/policyengine/core/tax_benefit_model_version.py +++ b/src/policyengine/core/tax_benefit_model_version.py @@ -4,8 +4,14 @@ from pydantic import BaseModel, Field -from .release_manifest import CountryReleaseManifest, DataCertification, PackageVersion +from .release_manifest import ( + CountryReleaseManifest, + DataCertification, + PackageVersion, + get_data_release_manifest, +) from .tax_benefit_model import TaxBenefitModel +from .trace_tro import build_trace_tro_from_release_bundle if TYPE_CHECKING: from .parameter import Parameter @@ -201,6 +207,22 @@ def release_bundle(self) -> dict[str, str | None]: ), } + @property + def trace_tro(self) -> dict: + if self.release_manifest is None: + raise ValueError( + "TRACE TRO export requires a bundled country release manifest." + ) + + data_release_manifest = get_data_release_manifest( + self.release_manifest.country_id + ) + return build_trace_tro_from_release_bundle( + self.release_manifest, + data_release_manifest, + certification=self.data_certification, + ) + def __repr__(self) -> str: # Give the id and version, and the number of variables, parameters, parameter nodes, parameter values return f"" diff --git a/src/policyengine/core/trace_tro.py b/src/policyengine/core/trace_tro.py new file mode 100644 index 00000000..0daa71f3 --- /dev/null +++ b/src/policyengine/core/trace_tro.py @@ -0,0 +1,258 @@ +from __future__ import annotations + +import hashlib +import json +from collections.abc import Iterable, Mapping + +from .release_manifest import ( + CountryReleaseManifest, + DataCertification, + DataReleaseManifest, +) + +TRACE_TROV_VERSION = "0.1" +TRACE_CONTEXT = [ + { + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "trov": "https://w3id.org/trace/trov/0.1#", + "schema": "https://schema.org/", + } +] + + +def _hash_object(value: str) -> dict[str, str]: + return { + "trov:hashAlgorithm": "sha256", + "trov:hashValue": value, + } + + +def _artifact_mime_type(path_or_uri: str) -> str | None: + suffix = path_or_uri.rsplit(".", 1)[-1].lower() if "." in path_or_uri else "" + return { + "h5": "application/x-hdf5", + "json": "application/json", + "jsonld": "application/ld+json", + }.get(suffix) + + +def _canonical_json_bytes(value: Mapping) -> bytes: + return (json.dumps(value, indent=2, sort_keys=True) + "\n").encode("utf-8") + + +def compute_trace_composition_fingerprint( + artifact_hashes: Iterable[str], +) -> str: + digest = hashlib.sha256() + digest.update("".join(sorted(artifact_hashes)).encode("utf-8")) + return digest.hexdigest() + + +def build_trace_tro_from_release_bundle( + country_manifest: CountryReleaseManifest, + data_release_manifest: DataReleaseManifest, + *, + certification: DataCertification | None = None, + bundle_manifest_path: str | None = None, + data_release_manifest_path: str | None = None, +) -> dict: + certified_artifact = country_manifest.certified_data_artifact + if certified_artifact is None: + raise ValueError("Country release manifest does not define a certified artifact.") + + dataset_artifact = data_release_manifest.artifacts.get(certified_artifact.dataset) + if dataset_artifact is None: + raise ValueError( + "Data release manifest does not include the certified dataset " + f"'{certified_artifact.dataset}'." + ) + if dataset_artifact.sha256 is None: + raise ValueError( + "Data release manifest does not include a SHA256 for the certified dataset " + f"'{certified_artifact.dataset}'." + ) + + effective_certification = certification or country_manifest.certification + bundle_manifest_location = ( + bundle_manifest_path + or f"data/release_manifests/{country_manifest.country_id}.json" + ) + data_manifest_location = data_release_manifest_path or ( + "https://huggingface.co/" + f"{country_manifest.data_package.repo_id}/resolve/" + f"{country_manifest.data_package.version}/" + f"{country_manifest.data_package.release_manifest_path}" + ) + + bundle_manifest_payload = country_manifest.model_dump(mode="json") + data_release_payload = data_release_manifest.model_dump(mode="json") + bundle_manifest_hash = hashlib.sha256( + _canonical_json_bytes(bundle_manifest_payload) + ).hexdigest() + data_release_manifest_hash = hashlib.sha256( + _canonical_json_bytes(data_release_payload) + ).hexdigest() + + artifact_specs = [ + { + "hash": bundle_manifest_hash, + "location": bundle_manifest_location, + "mime_type": "application/json", + }, + { + "hash": data_release_manifest_hash, + "location": data_manifest_location, + "mime_type": "application/json", + }, + { + "hash": dataset_artifact.sha256, + "location": certified_artifact.uri, + "mime_type": _artifact_mime_type(certified_artifact.uri), + }, + ] + + composition_artifacts = [] + arrangement_locations = [] + artifact_hashes = [] + + for index, artifact in enumerate(artifact_specs): + artifact_id = f"composition/1/artifact/{index}" + artifact_hashes.append(artifact["hash"]) + artifact_entry = { + "@id": artifact_id, + "@type": "trov:ResearchArtifact", + "trov:hash": _hash_object(artifact["hash"]), + } + if artifact["mime_type"] is not None: + artifact_entry["trov:mimeType"] = artifact["mime_type"] + composition_artifacts.append(artifact_entry) + arrangement_locations.append( + { + "@id": f"arrangement/0/location/{index}", + "@type": "trov:ArtifactLocation", + "trov:artifact": {"@id": artifact_id}, + "trov:path": artifact["location"], + } + ) + + certification_description = "" + if effective_certification is not None: + certification_description = ( + f" Certified for runtime model version " + f"{effective_certification.certified_for_model_version} via " + f"{effective_certification.compatibility_basis}." + ) + if effective_certification.built_with_model_version is not None: + certification_description += ( + f" Built with {country_manifest.model_package.name} " + f"{effective_certification.built_with_model_version}." + ) + if effective_certification.data_build_fingerprint is not None: + certification_description += ( + f" Data-build fingerprint: " + f"{effective_certification.data_build_fingerprint}." + ) + + created_at = country_manifest.published_at or ( + data_release_manifest.build.built_at + if data_release_manifest.build is not None + else None + ) + build_id = ( + effective_certification.data_build_id + if effective_certification is not None + else ( + certified_artifact.build_id + or f"{country_manifest.data_package.name}-{country_manifest.data_package.version}" + ) + ) + + return { + "@context": TRACE_CONTEXT, + "@graph": [ + { + "@id": "tro", + "@type": ["trov:TransparentResearchObject", "schema:CreativeWork"], + "trov:vocabularyVersion": TRACE_TROV_VERSION, + "schema:creator": country_manifest.policyengine_version, + "schema:name": ( + f"policyengine {country_manifest.country_id} " + f"certified bundle TRO" + ), + "schema:description": ( + f"TRACE TRO for certified runtime bundle " + f"{country_manifest.bundle_id or country_manifest.country_id} " + f"covering the bundled country release manifest, the country data " + f"release manifest, and the certified dataset artifact." + f"{certification_description}" + ), + "schema:dateCreated": created_at, + "trov:wasAssembledBy": { + "@id": "trs", + "@type": ["trov:TrustedResearchSystem", "schema:Organization"], + "schema:name": "PolicyEngine certified release bundle pipeline", + "schema:description": ( + "PolicyEngine certification workflow for runtime bundles that " + "pin a country model version, a country data release, and a " + "specific dataset artifact." + ), + }, + "trov:createdWith": { + "@type": "schema:SoftwareApplication", + "schema:name": "policyengine", + "schema:softwareVersion": country_manifest.policyengine_version, + }, + "trov:hasComposition": { + "@id": "composition/1", + "@type": "trov:ArtifactComposition", + "trov:hasFingerprint": { + "@id": "fingerprint", + "@type": "trov:CompositionFingerprint", + "trov:hash": _hash_object( + compute_trace_composition_fingerprint(artifact_hashes) + ), + }, + "trov:hasArtifact": composition_artifacts, + }, + "trov:hasArrangement": [ + { + "@id": "arrangement/0", + "@type": "trov:ArtifactArrangement", + "rdfs:comment": ( + f"Certified arrangement for bundle " + f"{country_manifest.bundle_id or country_manifest.country_id}." + ), + "trov:hasArtifactLocation": arrangement_locations, + } + ], + "trov:hasPerformance": [ + { + "@id": "trp/0", + "@type": "trov:TrustedResearchPerformance", + "rdfs:comment": ( + f"Certification of build {build_id} for " + f"{country_manifest.model_package.name} " + f"{country_manifest.model_package.version}." + ), + "trov:wasConductedBy": {"@id": "trs"}, + "trov:startedAtTime": ( + data_release_manifest.build.built_at + if data_release_manifest.build is not None + else created_at + ), + "trov:endedAtTime": created_at, + "trov:contributedToArrangement": { + "@id": "trp/0/binding/0", + "@type": "trov:ArrangementBinding", + "trov:arrangement": {"@id": "arrangement/0"}, + }, + } + ], + } + ], + } + + +def serialize_trace_tro(tro: Mapping) -> bytes: + return (json.dumps(tro, indent=2, sort_keys=True) + "\n").encode("utf-8") diff --git a/tests/test_release_manifests.py b/tests/test_release_manifests.py index 4a53fdb0..99b9bf10 100644 --- a/tests/test_release_manifests.py +++ b/tests/test_release_manifests.py @@ -4,6 +4,7 @@ from unittest.mock import MagicMock, patch from policyengine.core.release_manifest import ( + DataReleaseManifest, DataReleaseManifestUnavailable, certify_data_release_compatibility, dataset_logical_name, @@ -15,6 +16,11 @@ ) from policyengine.core.tax_benefit_model import TaxBenefitModel from policyengine.core.tax_benefit_model_version import TaxBenefitModelVersion +from policyengine.core.trace_tro import ( + build_trace_tro_from_release_bundle, + compute_trace_composition_fingerprint, + serialize_trace_tro, +) def _response_with_json(payload: dict) -> MagicMock: @@ -396,3 +402,186 @@ def test__given_runtime_certification__then_release_bundle_prefers_runtime_value assert bundle["data_build_fingerprint"] == "sha256:match" assert bundle["compatibility_basis"] == "matching_data_build_fingerprint" assert bundle["certified_by"] == "runtime certification" + + def test__given_same_hashes_in_different_orders__then_trace_fingerprint_matches(self): + hashes = ["ccc", "aaa", "bbb"] + + assert compute_trace_composition_fingerprint(hashes) == ( + compute_trace_composition_fingerprint(reversed(hashes)) + ) + + def test__given_release_bundle_and_data_manifest__then_trace_tro_tracks_bundle( + self, + ): + country_manifest = get_release_manifest("us") + data_release_manifest = DataReleaseManifest.model_validate( + { + "schema_version": 1, + "data_package": { + "name": "policyengine-us-data", + "version": "1.73.0", + }, + "build": { + "build_id": "policyengine-us-data-1.73.0", + "built_at": "2026-04-10T12:00:00Z", + "built_with_model_package": { + "name": "policyengine-us", + "version": "1.602.0", + "git_sha": "deadbeef", + "data_build_fingerprint": "sha256:build", + }, + }, + "compatible_model_packages": [], + "default_datasets": {"national": "enhanced_cps_2024"}, + "artifacts": { + "enhanced_cps_2024": { + "kind": "microdata", + "path": "enhanced_cps_2024.h5", + "repo_id": "policyengine/policyengine-us-data", + "revision": "1.73.0", + "sha256": "sha256-dataset", + "size_bytes": 123, + } + }, + } + ) + + tro = build_trace_tro_from_release_bundle( + country_manifest, + data_release_manifest, + ) + + graph = tro["@graph"][0] + artifacts = graph["trov:hasComposition"]["trov:hasArtifact"] + locations = graph["trov:hasArrangement"][0]["trov:hasArtifactLocation"] + + assert len(artifacts) == 3 + assert len(locations) == 3 + assert ( + graph["schema:description"] + == "TRACE TRO for certified runtime bundle us-3.4.0 covering the bundled country release manifest, the country data release manifest, and the certified dataset artifact. Certified for runtime model version 1.602.0 via exact_build_model_version. Built with policyengine-us 1.602.0." + ) + assert locations[0]["trov:path"] == "data/release_manifests/us.json" + assert ( + locations[1]["trov:path"] + == "https://huggingface.co/policyengine/policyengine-us-data/resolve/1.73.0/release_manifest.json" + ) + assert ( + locations[2]["trov:path"] + == "hf://policyengine/policyengine-us-data/enhanced_cps_2024.h5@1.73.0" + ) + assert ( + graph["trov:hasComposition"]["trov:hasFingerprint"]["trov:hash"][ + "trov:hashValue" + ] + == compute_trace_composition_fingerprint( + [ + artifact["trov:hash"]["trov:hashValue"] + for artifact in artifacts + ] + ) + ) + + def test__given_runtime_certification__then_trace_tro_uses_it(self): + manifest = get_release_manifest("us") + data_release_manifest = DataReleaseManifest.model_validate( + { + "schema_version": 1, + "data_package": { + "name": "policyengine-us-data", + "version": "1.73.0", + }, + "build": { + "build_id": "policyengine-us-data-1.73.0", + "built_at": "2026-04-10T12:00:00Z", + "built_with_model_package": { + "name": "policyengine-us", + "version": "1.602.0", + "git_sha": "deadbeef", + "data_build_fingerprint": "sha256:match", + }, + }, + "compatible_model_packages": [], + "default_datasets": {"national": "enhanced_cps_2024"}, + "artifacts": { + "enhanced_cps_2024": { + "kind": "microdata", + "path": "enhanced_cps_2024.h5", + "repo_id": "policyengine/policyengine-us-data", + "revision": "1.73.0", + "sha256": "sha256-dataset", + "size_bytes": 123, + } + }, + } + ) + model_version = TaxBenefitModelVersion( + model=TaxBenefitModel(id="us"), + version=manifest.model_package.version, + release_manifest=manifest, + model_package=manifest.model_package, + data_package=manifest.data_package, + default_dataset_uri=manifest.default_dataset_uri, + data_certification={ + "compatibility_basis": "matching_data_build_fingerprint", + "certified_for_model_version": "1.603.0", + "data_build_id": "policyengine-us-data-1.73.0", + "built_with_model_version": "1.602.0", + "built_with_model_git_sha": "deadbeef", + "data_build_fingerprint": "sha256:match", + "certified_by": "runtime certification", + }, + ) + + with patch( + "policyengine.core.tax_benefit_model_version.get_data_release_manifest", + return_value=data_release_manifest, + ): + tro = model_version.trace_tro + + description = tro["@graph"][0]["schema:description"] + + assert "Certified for runtime model version 1.603.0" in description + assert "via matching_data_build_fingerprint." in description + assert "Data-build fingerprint: sha256:match." in description + + def test__given_trace_tro__then_serialization_is_deterministic(self): + country_manifest = get_release_manifest("uk") + data_release_manifest = DataReleaseManifest.model_validate( + { + "schema_version": 1, + "data_package": { + "name": "policyengine-uk-data", + "version": "1.40.4", + }, + "build": { + "build_id": "policyengine-uk-data-1.40.4", + "built_at": "2026-04-10T12:00:00Z", + "built_with_model_package": { + "name": "policyengine-uk", + "version": "2.74.0", + "git_sha": "deadbeef", + "data_build_fingerprint": "sha256:build", + }, + }, + "compatible_model_packages": [], + "default_datasets": {"national": "enhanced_frs_2023_24"}, + "artifacts": { + "enhanced_frs_2023_24": { + "kind": "microdata", + "path": "enhanced_frs_2023_24.h5", + "repo_id": "policyengine/policyengine-uk-data-private", + "revision": "1.40.4", + "sha256": "sha256-dataset", + "size_bytes": 123, + } + }, + } + ) + + tro = build_trace_tro_from_release_bundle( + country_manifest, + data_release_manifest, + ) + + assert serialize_trace_tro(tro) == serialize_trace_tro(tro) From acebe35edcd619515270af24ac55fc28341e06bb Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 13 Apr 2026 13:23:04 -0400 Subject: [PATCH 2/4] Format TRACE TRO files --- src/policyengine/core/trace_tro.py | 7 ++++--- tests/test_release_manifests.py | 18 +++++++----------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/policyengine/core/trace_tro.py b/src/policyengine/core/trace_tro.py index 0daa71f3..52ae7b15 100644 --- a/src/policyengine/core/trace_tro.py +++ b/src/policyengine/core/trace_tro.py @@ -59,7 +59,9 @@ def build_trace_tro_from_release_bundle( ) -> dict: certified_artifact = country_manifest.certified_data_artifact if certified_artifact is None: - raise ValueError("Country release manifest does not define a certified artifact.") + raise ValueError( + "Country release manifest does not define a certified artifact." + ) dataset_artifact = data_release_manifest.artifacts.get(certified_artifact.dataset) if dataset_artifact is None: @@ -177,8 +179,7 @@ def build_trace_tro_from_release_bundle( "trov:vocabularyVersion": TRACE_TROV_VERSION, "schema:creator": country_manifest.policyengine_version, "schema:name": ( - f"policyengine {country_manifest.country_id} " - f"certified bundle TRO" + f"policyengine {country_manifest.country_id} certified bundle TRO" ), "schema:description": ( f"TRACE TRO for certified runtime bundle " diff --git a/tests/test_release_manifests.py b/tests/test_release_manifests.py index 99b9bf10..3ddadd61 100644 --- a/tests/test_release_manifests.py +++ b/tests/test_release_manifests.py @@ -403,7 +403,9 @@ def test__given_runtime_certification__then_release_bundle_prefers_runtime_value assert bundle["compatibility_basis"] == "matching_data_build_fingerprint" assert bundle["certified_by"] == "runtime certification" - def test__given_same_hashes_in_different_orders__then_trace_fingerprint_matches(self): + def test__given_same_hashes_in_different_orders__then_trace_fingerprint_matches( + self, + ): hashes = ["ccc", "aaa", "bbb"] assert compute_trace_composition_fingerprint(hashes) == ( @@ -470,16 +472,10 @@ def test__given_release_bundle_and_data_manifest__then_trace_tro_tracks_bundle( locations[2]["trov:path"] == "hf://policyengine/policyengine-us-data/enhanced_cps_2024.h5@1.73.0" ) - assert ( - graph["trov:hasComposition"]["trov:hasFingerprint"]["trov:hash"][ - "trov:hashValue" - ] - == compute_trace_composition_fingerprint( - [ - artifact["trov:hash"]["trov:hashValue"] - for artifact in artifacts - ] - ) + assert graph["trov:hasComposition"]["trov:hasFingerprint"]["trov:hash"][ + "trov:hashValue" + ] == compute_trace_composition_fingerprint( + [artifact["trov:hash"]["trov:hashValue"] for artifact in artifacts] ) def test__given_runtime_certification__then_trace_tro_uses_it(self): From 3d334b206ab4f538e82484b32513d093a64b0384 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 13 Apr 2026 14:28:21 -0400 Subject: [PATCH 3/4] Document TRACE bundle exports --- docs/release-bundles.md | 49 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/docs/release-bundles.md b/docs/release-bundles.md index d97e0370..56fb8075 100644 --- a/docs/release-bundles.md +++ b/docs/release-bundles.md @@ -186,6 +186,55 @@ Notes: - apps and APIs should surface this bundle, not only country package versions - a bundle may reuse a previously staged data artifact if compatibility is explicitly certified +## TRACE export + +The internal build manifest and certified runtime bundle remain the operational source of +truth. + +TRACE sits on top of those manifests as a standards-based export layer. + +### What gets exported + +Country `*-data` repos should emit a `trace.tro.jsonld` file for each published data +release. That TRO should cover: + +- the release manifest itself +- each published artifact hash listed in the release manifest +- the build-time model provenance recorded in the release manifest + +`policyengine.py` should emit a separate certified-bundle TRO. That TRO should cover: + +- the bundled country release manifest shipped in `policyengine.py` +- the country data release manifest resolved for the certified data package version +- the certified dataset artifact hash +- the certification basis used to allow runtime reuse + +### What TRACE does not replace + +TRACE is not the source of truth for compatibility policy. + +In particular, TRACE does not decide: + +- whether a new model version can safely reuse an existing data artifact +- how `data_build_fingerprint` is computed +- which staged artifact becomes a supported runtime default + +Those decisions still belong to the country data build manifest and the +`policyengine.py` certified runtime bundle. + +### Why we still want it + +TRACE adds three things our internal manifests do not provide by themselves: + +- a standard declaration format for provenance exchange +- a composition fingerprint over the exact artifacts in scope +- a better external surface for papers, audits, and reproducibility reviews + +That is why the recommended design is: + +- internal manifests for build/certification control +- generated TRACE TROs for standards-based export + ## Compatibility rule The architecture should avoid forcing a new data build for every harmless country model release. From 3eb71ac3e3303fdb4e5ab9f61602a427845001cd Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 13 Apr 2026 15:22:45 -0400 Subject: [PATCH 4/4] Modernize docs tooling commands --- .github/workflows/push.yaml | 4 +--- Makefile | 12 +++++++++--- README.md | 3 ++- docs/dev.md | 3 ++- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 43bec76f..7708fd9b 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -81,10 +81,8 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 18.x - - name: Install MyST - run: npm install -g mystmd - name: Build HTML Assets - run: cd docs && myst build --html + run: make docs - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: diff --git a/Makefile b/Makefile index f5028d39..f62643e1 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,15 @@ -.PHONY: docs +.PHONY: docs docs-serve + +MYSTMD_VERSION ?= 1.8.3 +MYST_CMD = npx --yes mystmd@$(MYSTMD_VERSION) all: build-package docs: - cd docs && jupyter book start + cd docs && $(MYST_CMD) build --html + +docs-serve: + cd docs && $(MYST_CMD) start install: uv pip install -e .[dev] @@ -27,4 +33,4 @@ build-package: python -m build test: - pytest tests --cov=policyengine --cov-report=term-missing \ No newline at end of file + pytest tests --cov=policyengine --cov-report=term-missing diff --git a/README.md b/README.md index 8d8917be..7fc607d5 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,8 @@ uv pip install -e .[dev] # install with dev dependencies (pytest, ruff, m ```bash make format # ruff format make test # pytest with coverage -make docs # build Jupyter Book documentation +make docs # build static MyST/Jupyter Book 2 HTML docs +make docs-serve # preview the docs locally make clean # remove caches, build artifacts, .h5 files ``` diff --git a/docs/dev.md b/docs/dev.md index 8fb0a140..007a94e5 100644 --- a/docs/dev.md +++ b/docs/dev.md @@ -23,7 +23,8 @@ dependencies used in CI (pytest, ruff, mypy, towncrier). ```bash make format # ruff format make test # pytest with coverage -make docs # run the MyST docs build used in CI via npx +make docs # build static MyST/Jupyter Book 2 HTML docs +make docs-serve # preview the docs locally make clean # remove caches, build artifacts, .h5 files ```