Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 1 addition & 2 deletions .binder/postBuild
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@ rm -rf rcpchgrowth/tests || true
rm -f rcpchgrowth/data_tables/bayley-pineau.pdf || true
rm -f rcpchgrowth/data_tables/uk-who_resources/GrowthCharts.xls || true
rm -f rcpchgrowth/data_tables/uk-who_resources/*.bas || true
rm -f rcpchgrowth/data_tables/fenton/*.* || true

echo "[postBuild] Warm up matplotlib font cache"
python - <<'PY'
import matplotlib.pyplot as plt
print("Matplotlib imported; font cache built.")
PY

echo "[postBuild] Done"
echo "[postBuild] Done"
1 change: 1 addition & 0 deletions .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ jobs:
[[ "${sdists[0]}" == "dist/rcpchgrowth-${EXPECTED_VERSION}.tar.gz" ]]
[[ "${wheels[0]}" == "dist/rcpchgrowth-${EXPECTED_VERSION}-"*.whl ]]
python -m twine check dist/*
python tests/distribution/check-sdist.py "${sdists[0]}"
python tests/distribution/check-wheel.py "${wheels[0]}"
python -m venv /tmp/release-wheel-venv
wheel_path="$PWD/${wheels[0]}"
Expand Down
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,3 @@ dist/
.private/
.playwright/
.playwright-mcp/

# Licensed data - Fenton chart data must not be shared
rcpchgrowth/data_tables/fenton/*.*
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ Release PRs may change only `pyproject.toml` and `CITATION.cff`, must carry exac

## Important Considerations for LLM Development

### Licensed Reference Data

Fenton growth-chart data, LMS tables, chart images, and copies of source publications must not be added to this repository, its Git history, build context, wheel, or source distribution unless a documented licence explicitly permits repository and PyPI redistribution. Public availability or publication of an article is not permission to redistribute it. Do not copy Fenton material from private workspaces or other repositories. Keep the package-data declaration as an explicit runtime allowlist, and keep distribution tests fail-closed against Fenton-named content and unapproved resources; `.gitignore` is not a distribution control.

### Test Fixture Strategy

The test fixture is **fixed and finite** (3984 cases). When modifying calculation logic:
Expand Down
22 changes: 19 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,18 +73,34 @@ select = ["E9", "F63", "F7", "F82"]
"rcpchgrowth/dynamic_growth.py" = ["F821"]

[tool.setuptools]
include-package-data = true
include-package-data = false

[tool.setuptools.packages.find]
include = ["rcpchgrowth*"]
exclude = ["notebooks"]
exclude = ["notebooks", "rcpchgrowth.tests", "rcpchgrowth.tests.*"]

[project.readme]
file = "README.md"
content-type = "text/markdown"

[tool.setuptools.package-data]
rcpchgrowth = ["data_tables/**/*.csv", "data_tables/**/*.json", "data_tables/**/*.xls", "data_tables/**/*.pdf", "data_tables/**/*.bas", "data_tables/**/*.xlsx"]
rcpchgrowth = [
"data_tables/cdc2-20.json",
"data_tables/cdc_infants.json",
"data_tables/trisomy_21.json",
"data_tables/trisomy_21_aap_children.json",
"data_tables/trisomy_21_aap_infants.json",
"data_tables/turner.json",
"data_tables/uk90_child.json",
"data_tables/uk90_preterm.json",
"data_tables/uk90_term.json",
"data_tables/uk_who_weight_correlation_matrices/weight_correlation_by_month.json",
"data_tables/uk_who_weight_correlation_matrices/weight_correlation_by_week.json",
"data_tables/who/pre_2025/who_infants.json",
"data_tables/who/who_2007_children.json",
"data_tables/who/who_children.json",
"data_tables/who/who_infants.json",
]

# Version bumping. `version` above is the single source of truth; this keeps
# the duplicated version in CITATION.cff (and its release date) in sync so it
Expand Down
Binary file not shown.
9 changes: 8 additions & 1 deletion s/test-wheel
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,14 @@ if (($#)); then
docker compose cp "$host_wheel" "rcpchgrowth-python:$work_dir/dist/$wheel_name" >/dev/null
wheel_path="$work_dir/dist/$wheel_name"
else
docker compose exec -T rcpchgrowth-python python -m build --wheel --outdir "$work_dir/dist"
docker compose exec -T rcpchgrowth-python cp -a /app "$work_dir/source"
docker compose exec -T rcpchgrowth-python rm -rf \
"$work_dir/source/build" \
"$work_dir/source/dist" \
"$work_dir/source/rcpchgrowth.egg-info"
docker compose exec -T rcpchgrowth-python sh -c \
'cd "$1" && exec python -m build --wheel --outdir "$2"' \
sh "$work_dir/source" "$work_dir/dist"
wheel_path="$(docker compose exec -T rcpchgrowth-python sh -c 'set -- "$1"/*.whl; test "$#" -eq 1; printf "%s" "$1"' sh "$work_dir/dist")"
fi

Expand Down
14 changes: 7 additions & 7 deletions spec/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Complete these items before broad formatting, import cleanup, packaging reductio
- [x] **R19 - Characterize serialized measurement contracts.** Add golden or schema-focused tests for the complete `Measurement.measurement` dictionary across representative successful, corrected-age, out-of-range, and validation-error cases. Assert key presence, nesting, nullability, value types, reference identifiers, and clinically significant numeric values.
- [~] **R20 - Fill calculation-vector and boundary gaps.** Independent representative vectors and documented error boundaries now cover every supported reference. Finalize the exact 23-, 25-, and 42-week boundaries through the existing PR #101 path recorded in `spec/queries.md`.
- [~] **R21 - Characterize chart-builder outputs.** Direct tests now cover structure, series selection, age domains, boundary ownership, and representative coordinates for every supported reference. Finalize the exact preterm chart boundaries through the existing PR #101 path recorded in `spec/queries.md`.
- [~] **R22 - Add installed-wheel smoke and resource tests.** Build and install the candidate wheel into an isolated environment, verify public imports and version metadata, and execute representative calculations that load every required packaged data family. The isolated smoke and required-resource checks are active locally and in CI; finish the forbidden-content assertions after R4-R8 establish the approved package policy.
- [x] **R22 - Add installed-wheel smoke and resource tests.** Build and install the candidate wheel into an isolated environment, verify public imports and version metadata, and execute representative calculations that load every required packaged data family. The isolated smoke and exact resource-allowlist checks reject packaged tests, generated files, notebooks, unapproved resources, and Fenton-named content.
- [~] **R23 - Add a downstream candidate-wheel compatibility gate.** The exact-wheel runner validates artifact hashes and provenance, the server calculation/API regression suite, dynamic charts, and both pinned React profiles against an immutable server revision. The runner passed against PR #280 commit `6918e3b8152963345f4c0996e7bd52bacf904583` using the published 4.6.0 wheel; repeat it with a commit-stamped candidate wheel against PR #280's immutable merge commit after that PR adopts the canonical engine identifier, then use that merge commit as the permanent release/CI pin.

## High priority
Expand All @@ -39,11 +39,11 @@ Complete these items before broad formatting, import cleanup, packaging reductio

- [ ] **R25 - Derive accepted age domains from reference metadata and table extents.** Replace duplicated age-limit literals with a central domain model for every reference segment, measurement, and sex. Distinguish observed table extent from clinically supported applicability, including endpoint inclusivity and overlap precedence for hybrid references such as UK-WHO and WHO. Add build-time checks against canonical source material in `rcpch/growth-references`, prohibit extrapolation beyond selected series, and test every first/last accepted endpoint plus `math.nextafter()` immediately outside it. Track implementation in issue #109.
- [ ] **R3 - Decide the future of the unusable `BoneAge` class.** `rcpchgrowth/bone_age.py:12-48` returns a dictionary from `__init__`, so construction raises `TypeError: __init__() should return None`. The class has no internal consumers and is not exported from the package root, although related fields are supported by `Measurement`. Either implement and test a coherent object API or remove the module after checking external compatibility.
- [ ] **R4 - Define an explicit runtime package-data allowlist.** The published installation occupies roughly 15 MB and ships spreadsheets, a PDF, VBA source, validation CSVs, old WHO tables, experimental LMS tables, and other provenance material because `pyproject.toml:68-69` broadly includes file extensions beneath `data_tables`. Identify the JSON and correlation resources required at runtime, package only those files, and verify wheel and sdist contents in CI.
- [ ] **R5 - Exclude the test package from release wheels.** `pyproject.toml:60-62` discovers `rcpchgrowth*`, which includes 19 modules beneath `rcpchgrowth/tests` in the published wheel. Exclude `rcpchgrowth.tests` and its subpackages while retaining all tests in the source repository and sdist if deliberately required there.
- [ ] **R6 - Separate runtime data from source and validation provenance.** Review `rcpchgrowth/data_tables/uk-who_resources/` (about 6.2 MB), `test_lms_cole_tmp/`, `validation_who_vs_uk_who/`, `cdc_resources/`, WHO CSV sources, `who/pre_2025/`, AAP spreadsheets, `GrowthCharts.xls`, `huiqi_cole_methods.bas`, and `data_tables.txt`. Retain required provenance in the repository or documentation project, but move it outside runtime package data and document what generated each shipped table.
- [ ] **R7 - Retire or archive the legacy `cdc.json` table.** `rcpchgrowth/data_tables/cdc.json` is approximately 308 KB and is shipped, but runtime code uses `cdc_infants.json` and `cdc2-20.json`. Confirm that no external resource consumer relies on its path, then remove it from package data or move it to a clearly documented provenance archive.
- [ ] **R8 - Decide whether height-prediction data is a supported feature.** The Bayley-Pinneau CSV files and `constants/height_predictions_constants.py` are shipped and indirectly exposed through wildcard constant imports, but no implementation or internal consumer exists. Either implement and test the feature or stop packaging and exporting these placeholders after a compatibility review.
- [x] **R4 - Define an explicit runtime package-data allowlist.** Packaging now includes only the 15 JSON resources traced to production loads, and the artifact checkers reject every unapproved package resource.
- [x] **R5 - Exclude the test package from release wheels.** Package discovery excludes `rcpchgrowth.tests` and its subpackages, and the wheel checker rejects packaged test modules.
- [~] **R6 - Separate runtime data from source and validation provenance.** Source spreadsheets, validation CSVs, VBA, intermediate tables, and other provenance remain available in the repository but are excluded from runtime wheels. Finish documenting the generation and provenance of each shipped runtime table.
- [x] **R7 - Retire or archive the legacy `cdc.json` table.** The unused aggregate remains available as repository provenance but is excluded from package data; runtime continues to use `cdc_infants.json` and `cdc2-20.json`.
- [~] **R8 - Decide whether height-prediction data is a supported feature.** Bayley-Pinneau CSV files are no longer shipped in runtime wheels. Decide separately whether to implement the feature or remove its indirectly exposed placeholder constants through a compatibility-managed change.

## Low priority

Expand All @@ -52,7 +52,7 @@ Complete these items before broad formatting, import cleanup, packaging reductio
- [ ] **R11 - Remove confirmed unused imports and duplicate imports.** Clean `json` from `global_functions.py`; `CDC`, `HEIGHT`, `WEIGHT`, `rounded_sds_for_centile`, and duplicate `HEAD_CIRCUMFERENCE`/`BMI` imports from `centile_bands.py`; `FENTON` and `UK_WHO_CHILD` from `chart_functions.py`; `FEMALE`, `MALE`, and `z_score` from `dynamic_growth.py`; `scipy.stats.norm` from `test_centile_band_for_centile.py`; and `json`, `pprint`, and the local `math` import from `test_who.py`. Do not remove the root-package imports that intentionally define the public API.
- [ ] **R12 - Remove unused WHO precalculated centile arrays after source review.** The large arrays `WHO_GIRL_WEIGHT_UNDER_FIVE_15`, `WHO_GIRL_BMI_OVER_FIVE_15`, `WHO_BOY_LENGTH_OVER_FIVE_15`, `WHO_BOY_WEIGHT_OVER_FIVE_15`, and `WHO_BOY_BMI_OVER_FIVE_15` in `rcpchgrowth/tests/who_test_data/who_chart_precalculated_centiles.py` have no test consumers. Confirm that they are not intentionally retained source vectors before deleting them.
- [ ] **R13 - Make the public namespace explicit.** Add deliberate `__all__` declarations to `rcpchgrowth/__init__.py` and the constants package, then replace internal wildcard imports incrementally. Preserve the existing supported names unless a breaking API change is explicitly approved; the current Ruff reports for root re-exports are expected false positives rather than automatic deletion candidates.
- [ ] **R14 - Make release builds isolated and reproducible.** A local wheel built with an existing ignored `rcpchgrowth.egg-info/SOURCES.txt` contained `data_tables.txt`, while the published wheel did not, demonstrating that stale build metadata can affect package contents. Build releases from a clean source tree, inspect wheel and sdist manifests automatically, and ensure ignored metadata cannot alter artifacts.
- [x] **R14 - Make release builds isolated and reproducible.** Release CI builds from a clean checkout and validates both wheel and sdist manifests. Local wheel tests build from a temporary clean copy of the current worktree, preventing ignored `build/` and `rcpchgrowth.egg-info/` state from altering artifacts.
- [ ] **R15 - Clear disposable generated artifacts from development worktrees.** The audited worktree contained ignored `build/`, `dist/`, `rcpchgrowth.egg-info/`, `.pytest_cache/`, `__pycache__/`, and notebook checkpoint directories, including an obsolete `2.1.9` distribution artifact. Provide or document a safe cleanup command that removes only ignored generated output and never reference or provenance data.

## Dependency audit result
Expand Down
90 changes: 90 additions & 0 deletions tests/distribution/check-sdist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""Validate source distribution structure and package contents."""

import sys
from email.parser import Parser
from pathlib import Path
from runpy import run_path
from tarfile import open as open_tar


WHEEL_CHECKER = Path(__file__).with_name("check-wheel.py")
WHEEL_POLICY = run_path(str(WHEEL_CHECKER))
RUNTIME_RESOURCES = WHEEL_POLICY["RUNTIME_RESOURCES"]
REQUIRED_FILES = WHEEL_POLICY["REQUIRED_FILES"] | {
"LICENSE",
"README.md",
"pyproject.toml",
}
ALLOWED_BUILD_METADATA = {
"PKG-INFO",
"rcpchgrowth.egg-info/PKG-INFO",
"rcpchgrowth.egg-info/SOURCES.txt",
"rcpchgrowth.egg-info/dependency_links.txt",
"rcpchgrowth.egg-info/requires.txt",
"rcpchgrowth.egg-info/top_level.txt",
"setup.cfg",
}


def main() -> None:
sdist = Path(sys.argv[1]).resolve()
if not sdist.is_file() or not sdist.name.endswith(".tar.gz"):
raise SystemExit(f"Not a source distribution: {sdist}")

with open_tar(sdist, "r:gz") as archive:
file_members = {member.name: member for member in archive if member.isfile()}
roots = {Path(name).parts[0] for name in file_members}
if len(roots) != 1:
raise AssertionError(f"Expected one sdist root, found {sorted(roots)}")
root = roots.pop()
metadata_name = f"{root}/PKG-INFO"
if metadata_name not in file_members:
raise AssertionError(f"Missing sdist metadata: {metadata_name}")
metadata_file = archive.extractfile(file_members[metadata_name])
assert metadata_file is not None
metadata = Parser().parsestr(metadata_file.read().decode())

members = {name.removeprefix(f"{root}/") for name in file_members}
missing = sorted(REQUIRED_FILES - members)
unexpected_resources = sorted(
name
for name in members
if not name.endswith(".py")
and name not in REQUIRED_FILES
and name not in ALLOWED_BUILD_METADATA
)
forbidden = sorted(
name
for name in members
if "__pycache__" in name
or name.endswith((".pyc", ".pyo"))
or "notebooks" in Path(name).parts
or name.startswith("rcpchgrowth/tests/")
or "fenton" in name.casefold()
)
assert not missing, f"Required files missing from sdist: {missing}"
assert not unexpected_resources, (
f"Unapproved package resources found in sdist: {unexpected_resources}"
)
assert not forbidden, f"Forbidden files found in sdist: {forbidden}"
assert metadata["Name"] == "rcpchgrowth"
assert metadata["Version"]
expected_name = f"rcpchgrowth-{metadata['Version']}.tar.gz"
expected_root = f"rcpchgrowth-{metadata['Version']}"
assert root == expected_root, (
f"Source distribution root and metadata version differ: "
f"{root}, {metadata['Version']}"
)
assert sdist.name == expected_name, (
f"Source distribution filename and metadata version differ: "
f"{sdist.name}, {metadata['Version']}"
)
print(
f"Validated {sdist.name}: rcpchgrowth {metadata['Version']}, "
f"{len(members)} files"
)


if __name__ == "__main__":
main()
21 changes: 18 additions & 3 deletions tests/distribution/check-wheel.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
#!/usr/bin/env python3
"""Validate the minimum wheel structure needed by runtime consumers."""
"""Validate the exact wheel structure needed by runtime consumers."""

import sys
from email.parser import Parser
from pathlib import Path
from zipfile import ZipFile

REQUIRED_FILES = {
REQUIRED_PYTHON_FILES = {
"rcpchgrowth/__init__.py",
"rcpchgrowth/_build_info.py",
}
RUNTIME_RESOURCES = {
"rcpchgrowth/data_tables/cdc2-20.json",
"rcpchgrowth/data_tables/cdc_infants.json",
"rcpchgrowth/data_tables/trisomy_21.json",
Expand All @@ -25,6 +27,7 @@
"rcpchgrowth/data_tables/who/who_children.json",
"rcpchgrowth/data_tables/who/who_infants.json",
}
REQUIRED_FILES = REQUIRED_PYTHON_FILES | RUNTIME_RESOURCES


def main() -> None:
Expand All @@ -42,15 +45,27 @@ def main() -> None:
metadata = Parser().parsestr(archive.read(metadata_names[0]).decode())

missing = sorted(REQUIRED_FILES - members)
package_resources = {
name
for name in members
if name.startswith("rcpchgrowth/")
and not name.endswith((".py", "/"))
}
unexpected_resources = sorted(package_resources - RUNTIME_RESOURCES)
forbidden = sorted(
name
for name in members
if "__pycache__" in name
or name.endswith((".pyc", ".pyo"))
or "notebooks" in Path(name).parts
or name.startswith("rcpchgrowth/tests/")
or "fenton" in name.casefold()
)
assert not missing, f"Required runtime files missing from wheel: {missing}"
assert not forbidden, f"Generated or notebook files found in wheel: {forbidden}"
assert not unexpected_resources, (
f"Unapproved package resources found in wheel: {unexpected_resources}"
)
assert not forbidden, f"Forbidden files found in wheel: {forbidden}"
assert metadata["Name"] == "rcpchgrowth"
assert metadata["Version"]
assert wheel.name.startswith(f"rcpchgrowth-{metadata['Version']}-"), (
Expand Down
Loading