Skip to content

Commit 77458c0

Browse files
committed
Add typed stage 5 promotion results
1 parent f422d93 commit 77458c0

16 files changed

Lines changed: 1486 additions & 10 deletions

File tree

changelog.d/1026.added

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added typed Stage 5 promotion result models around the existing release transaction engine.

docs/engineering/pipeline-map.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -882,6 +882,14 @@ def fitted_weights_spec_for_scope(scope: FitScope | str) -> FittedWeightsSpec
882882

883883
Return the current fitted-weight spec for a regional or national scope.
884884

885+
### `policyengine_us_data.release_promotion.results.full.FullPromotionResult`
886+
887+
```python
888+
class FullPromotionResult
889+
```
890+
891+
Typed result for a full Stage 5 release promotion transaction.
892+
885893
### `policyengine_us_data.datasets.cps.extended_cps.ExtendedCPS._validate_housing_assistance_microsimulation`
886894

887895
```python
@@ -1450,6 +1458,14 @@ def stage_1_step_specs() -> tuple[DatasetBuildStepSpec, ...]
14501458

14511459
Return the canonical Stage 1 dataset-build substage specs.
14521460

1461+
### `policyengine_us_data.utils.release_promotion.promote_full_release_with_result`
1462+
1463+
```python
1464+
def promote_full_release_with_result(config: FullReleasePromotionConfig, deps: FullReleasePromotionDependencies) -> 'FullPromotionResult'
1465+
```
1466+
1467+
Run the existing transaction engine and wrap its output in a typed result.
1468+
14531469
### `policyengine_us_data.calibration.unified_matrix_builder.UnifiedMatrixBuilder`
14541470

14551471
```python

docs/engineering/skills/documentation_review.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ Check that changed pipeline behavior has a durable documentation surface:
4747
- Generated docs build when decorator, Pydoc, or map source changes. PRs that
4848
change decorator metadata, Pydoc-facing source, or `docs/pipeline_map.yaml`
4949
should refresh the checked-in generated artifacts in the same change.
50+
- Pipeline documentation segment edits require the same treatment: if a PR
51+
changes source text that feeds generated pipeline docs or AI-facing pipeline
52+
guidance, verify whether `scripts/extract_pipeline_docs.py` output changes and
53+
commit the refreshed generated artifacts when it does.
5054
- Stale architecture names, folder names, and artifact names are not preserved in
5155
durable documentation sources or generated output.
5256

docs/engineering/skills/pipeline_docs.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,14 @@ The generated JSON and Markdown files are published artifacts, not hand-authored
1919
source. PRs should update decorators, docstrings, and `docs/pipeline_map.yaml`,
2020
then regenerate the checked-in artifacts in the same change so reviewers see the
2121
pipeline docs that will ship. On pushes to `main`, automation may refresh those
22-
artifacts again with the version/changelog commit.
22+
artifacts again with the version/changelog commit, but PR authors and AI agents
23+
must not rely on that later automation for review correctness.
24+
25+
Any time a PR touches a pipeline documentation segment, a `@pipeline_node`
26+
decorator, Pydoc-facing text that feeds the extractor, or
27+
`docs/pipeline_map.yaml`, regenerate and commit the checked-in docs produced by
28+
`scripts/extract_pipeline_docs.py`. Treat the generated docs as part of the same
29+
change, even if the source edit is small.
2330

2431
## Annotation Rules
2532

docs/engineering/stages/release_promotion.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,21 @@ perform Hugging Face writes, GCS uploads, Modal calls, staging cleanup, or
103103
release-manifest publication. Keep those operations behind explicit adapters or
104104
services so tests can exercise candidate shape and validation logic without
105105
credentials or network access.
106+
107+
Use `FullPromotionResult` and its substep result objects when exposing Stage 5
108+
promotion outcomes to contracts, status APIs, or orchestration summaries. The
109+
current compatibility wrapper, `promote_full_release_with_result()`, must keep
110+
calling the existing transaction engine first and only wrap its dictionary
111+
output afterward so the promotion order remains unchanged.
112+
113+
Result objects should carry semantic public-output identity, not only counts.
114+
Keep Hugging Face repo/type, staging prefix, promoted/no-op paths, and commit ID
115+
when available on `HuggingFacePromotionResult`; GCS bucket, release version,
116+
object paths, skipped paths, and failures on `GcsPromotionResult`; release
117+
manifest and TRACE TRO paths on `ReleaseManifestPromotionResult`; version
118+
manifest path/version/current version on `VersionManifestPromotionResult`;
119+
completion marker path/tag/validity on `CompletionMarkerPromotionResult`; and
120+
cleanup `status` as `skipped`, `completed`, or `failed` on
121+
`CleanupPromotionResult`. Later contract, index, diagnostics, and status
122+
writers should read this typed material instead of scraping logs or
123+
reconstructing public paths independently.

docs/generated/pipeline_api.json

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1228,6 +1228,40 @@
12281228
"signature": "def fitted_weights_spec_for_scope(scope: FitScope | str) -> FittedWeightsSpec",
12291229
"source_file": "policyengine_us_data/fit_weights/specs.py"
12301230
},
1231+
"full_promotion_result": {
1232+
"docstring": "Typed result for a full Stage 5 release promotion transaction.",
1233+
"id": "full_promotion_result",
1234+
"kind": "class",
1235+
"line": 62,
1236+
"metadata": {
1237+
"api_refs": [
1238+
"policyengine_us_data.release_promotion.results.FullPromotionResult",
1239+
"policyengine_us_data.release_promotion.results.full.FullPromotionResult"
1240+
],
1241+
"artifacts_in": [
1242+
"release promotion transaction output"
1243+
],
1244+
"artifacts_out": [
1245+
"typed promotion result"
1246+
],
1247+
"description": "Typed Stage 5 result model for full release promotion outcomes.",
1248+
"id": "full_promotion_result",
1249+
"label": "FullPromotionResult",
1250+
"node_type": "library",
1251+
"pathways": [
1252+
"5_validate_and_promote_release"
1253+
],
1254+
"source_file": "policyengine_us_data/release_promotion/results/full.py",
1255+
"stability": "moving",
1256+
"status": "transitional",
1257+
"validation_commands": [
1258+
"uv run pytest tests/unit/release_promotion/test_results.py"
1259+
]
1260+
},
1261+
"object_path": "policyengine_us_data.release_promotion.results.full.FullPromotionResult",
1262+
"signature": "class FullPromotionResult",
1263+
"source_file": "policyengine_us_data/release_promotion/results/full.py"
1264+
},
12311265
"geo_assign": {
12321266
"docstring": "Assign random census block geography to cloned\nCPS records.\n\nEach of n_records * n_clones total records gets a\nrandom census block sampled from the global\npopulation-weighted distribution. State and CD are\nderived from the block GEOID.\n\nArgs:\n n_records: Number of households in the base CPS\n dataset.\n n_clones: Number of clones (default 10).\n seed: Random seed for reproducibility.\n fixed_state_fips: Optional state FIPS per base record. Positive\n values constrain every clone of that record to blocks in the\n requested state; zero or missing values remain unrestricted.\n\nReturns:\n GeographyAssignment with arrays of length\n n_records * n_clones.",
12331267
"id": "geo_assign",
@@ -3889,6 +3923,40 @@
38893923
"signature": "def validate_area(sim, targets_df: pd.DataFrame, engine, area_type: str, area_id: str, display_id: str, dataset_path: str, period: int, training_mask: np.ndarray, variable_entity_map: dict, constraints_map: Optional[dict] = None) -> list",
38903924
"source_file": "policyengine_us_data/calibration/validate_staging.py"
38913925
},
3926+
"typed_full_release_promotion": {
3927+
"docstring": "Run the existing transaction engine and wrap its output in a typed result.",
3928+
"id": "typed_full_release_promotion",
3929+
"kind": "function",
3930+
"line": 171,
3931+
"metadata": {
3932+
"api_refs": [
3933+
"policyengine_us_data.utils.release_promotion.promote_full_release_with_result"
3934+
],
3935+
"artifacts_in": [
3936+
"staged release artifacts",
3937+
"release manifest inputs"
3938+
],
3939+
"artifacts_out": [
3940+
"FullPromotionResult"
3941+
],
3942+
"description": "Compatibility wrapper that returns typed Stage 5 promotion results from the existing transaction engine.",
3943+
"id": "typed_full_release_promotion",
3944+
"label": "Typed Full Release Promotion",
3945+
"node_type": "library",
3946+
"pathways": [
3947+
"5_validate_and_promote_release"
3948+
],
3949+
"source_file": "policyengine_us_data/utils/release_promotion.py",
3950+
"stability": "moving",
3951+
"status": "transitional",
3952+
"validation_commands": [
3953+
"uv run pytest tests/unit/release_promotion/test_results.py"
3954+
]
3955+
},
3956+
"object_path": "policyengine_us_data.utils.release_promotion.promote_full_release_with_result",
3957+
"signature": "def promote_full_release_with_result(config: FullReleasePromotionConfig, deps: FullReleasePromotionDependencies) -> 'FullPromotionResult'",
3958+
"source_file": "policyengine_us_data/utils/release_promotion.py"
3959+
},
38923960
"unified_matrix_builder": {
38933961
"docstring": "Build sparse calibration matrix for cloned CPS records.\n\nProcesses clone-by-clone: each clone's records get their\nassigned geography, are simulated, and the results fill\nthe corresponding columns.\n\nArgs:\n db_uri: SQLAlchemy database URI.\n time_period: Tax year for calibration (e.g. 2024).\n dataset_path: Path to the base extended CPS h5 file.",
38943962
"id": "unified_matrix_builder",

docs/generated/pipeline_map.json

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,31 @@
344344
"uv run pytest tests/unit/fit_weights/test_specs.py"
345345
]
346346
},
347+
{
348+
"api_refs": [
349+
"policyengine_us_data.release_promotion.results.FullPromotionResult",
350+
"policyengine_us_data.release_promotion.results.full.FullPromotionResult"
351+
],
352+
"artifacts_in": [
353+
"release promotion transaction output"
354+
],
355+
"artifacts_out": [
356+
"typed promotion result"
357+
],
358+
"description": "Typed Stage 5 result model for full release promotion outcomes.",
359+
"id": "full_promotion_result",
360+
"label": "FullPromotionResult",
361+
"node_type": "library",
362+
"pathways": [
363+
"5_validate_and_promote_release"
364+
],
365+
"source_file": "policyengine_us_data/release_promotion/results/full.py",
366+
"stability": "moving",
367+
"status": "transitional",
368+
"validation_commands": [
369+
"uv run pytest tests/unit/release_promotion/test_results.py"
370+
]
371+
},
347372
{
348373
"api_refs": [
349374
"policyengine_us_data.datasets.cps.extended_cps.ExtendedCPS._validate_housing_assistance_microsimulation"
@@ -1741,6 +1766,31 @@
17411766
"uv run pytest tests/unit/test_build_dataset_specs.py"
17421767
]
17431768
},
1769+
{
1770+
"api_refs": [
1771+
"policyengine_us_data.utils.release_promotion.promote_full_release_with_result"
1772+
],
1773+
"artifacts_in": [
1774+
"staged release artifacts",
1775+
"release manifest inputs"
1776+
],
1777+
"artifacts_out": [
1778+
"FullPromotionResult"
1779+
],
1780+
"description": "Compatibility wrapper that returns typed Stage 5 promotion results from the existing transaction engine.",
1781+
"id": "typed_full_release_promotion",
1782+
"label": "Typed Full Release Promotion",
1783+
"node_type": "library",
1784+
"pathways": [
1785+
"5_validate_and_promote_release"
1786+
],
1787+
"source_file": "policyengine_us_data/utils/release_promotion.py",
1788+
"stability": "moving",
1789+
"status": "transitional",
1790+
"validation_commands": [
1791+
"uv run pytest tests/unit/release_promotion/test_results.py"
1792+
]
1793+
},
17441794
{
17451795
"api_refs": [
17461796
"policyengine_us_data.calibration.unified_matrix_builder.UnifiedMatrixBuilder"
@@ -1966,9 +2016,9 @@
19662016
}
19672017
],
19682018
"metadata": {
1969-
"api_node_count": 95,
2019+
"api_node_count": 97,
19702020
"canonical_stage_count": 5,
1971-
"decorated_object_count": 144,
2021+
"decorated_object_count": 146,
19722022
"mapped_decorated_node_count": 49,
19732023
"stage_count": 17,
19742024
"substage_count": 17

policyengine_us_data/release_promotion/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@
2424
build_release_candidate_bundle_from_stage4_contract,
2525
read_stage4_release_candidate_bundle,
2626
)
27+
from .results import (
28+
CleanupPromotionResult,
29+
CompletionMarkerPromotionResult,
30+
FullPromotionResult,
31+
GcsPromotionResult,
32+
HuggingFacePromotionResult,
33+
ReleaseManifestPromotionResult,
34+
VersionManifestPromotionResult,
35+
)
2736
from .validation import build_release_candidate_shape_report
2837
from .validation import (
2938
DEFAULT_REQUIRED_RELEASE_ARTIFACT_FAMILIES,
@@ -39,11 +48,18 @@
3948
"BASE_RELEASE_ARTIFACT_PATHS",
4049
"DEFAULT_REQUIRED_RELEASE_ARTIFACT_FAMILIES",
4150
"RELEASE_VALIDATION_SUBSTAGE_ID",
51+
"CleanupPromotionResult",
52+
"CompletionMarkerPromotionResult",
53+
"FullPromotionResult",
54+
"GcsPromotionResult",
55+
"HuggingFacePromotionResult",
4256
"ReleaseArtifactSpec",
4357
"ReleaseCandidateInputBundle",
4458
"ReleasePromotionContext",
4559
"ReleaseCandidateValidationDependencies",
4660
"ReleaseCandidateValidator",
61+
"ReleaseManifestPromotionResult",
62+
"VersionManifestPromotionResult",
4763
"VALIDATION_REPORT_POLICY_PRESENCE_ONLY",
4864
"VALIDATION_REPORT_POLICY_REQUIRE_PASSING",
4965
"build_legacy_release_candidate_bundle",
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Typed Stage 5 promotion result models."""
2+
3+
from .cleanup import (
4+
CLEANUP_STATUS_COMPLETED,
5+
CLEANUP_STATUS_FAILED,
6+
CLEANUP_STATUS_SKIPPED,
7+
CLEANUP_STATUSES,
8+
CleanupPromotionResult,
9+
)
10+
from .destinations import (
11+
GcsPromotionResult,
12+
HuggingFacePromotionResult,
13+
)
14+
from .full import FullPromotionResult
15+
from .manifests import (
16+
CompletionMarkerPromotionResult,
17+
ReleaseManifestPromotionResult,
18+
VersionManifestPromotionResult,
19+
)
20+
21+
__all__ = [
22+
"CLEANUP_STATUS_COMPLETED",
23+
"CLEANUP_STATUS_FAILED",
24+
"CLEANUP_STATUS_SKIPPED",
25+
"CLEANUP_STATUSES",
26+
"CleanupPromotionResult",
27+
"CompletionMarkerPromotionResult",
28+
"FullPromotionResult",
29+
"GcsPromotionResult",
30+
"HuggingFacePromotionResult",
31+
"ReleaseManifestPromotionResult",
32+
"VersionManifestPromotionResult",
33+
]
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""Shared coercion helpers for typed release-promotion results."""
2+
3+
from __future__ import annotations
4+
5+
from collections.abc import Sequence
6+
from typing import Any
7+
8+
from policyengine_us_data.stage_contracts._coercion import require_non_empty
9+
10+
11+
def nonnegative_int(value: Any, field_name: str) -> int:
12+
"""Return a non-negative integer or raise a contract validation error."""
13+
14+
if isinstance(value, bool) or not isinstance(value, int):
15+
raise ValueError(f"{field_name} must be an integer")
16+
if value < 0:
17+
raise ValueError(f"{field_name} must be non-negative")
18+
return value
19+
20+
21+
def bool_value(value: Any, field_name: str) -> bool:
22+
"""Return a boolean or raise a contract validation error."""
23+
24+
if not isinstance(value, bool):
25+
raise ValueError(f"{field_name} must be a boolean")
26+
return value
27+
28+
29+
def string_tuple(value: Any, field_name: str) -> tuple[str, ...]:
30+
"""Return a tuple of non-empty strings from a sequence value."""
31+
32+
if value is None:
33+
return ()
34+
if isinstance(value, str) or not isinstance(value, Sequence):
35+
raise ValueError(f"{field_name} must be a sequence of strings")
36+
items = tuple(value)
37+
for item in items:
38+
require_non_empty(item, f"{field_name} item")
39+
return items
40+
41+
42+
def require_type(value: Any, field_name: str, expected_type: type) -> None:
43+
"""Validate a nested typed result object."""
44+
45+
if not isinstance(value, expected_type):
46+
raise ValueError(f"{field_name} must be {expected_type.__name__}")

0 commit comments

Comments
 (0)