From 9c3dd80b5c7da3cd3085750d0b2a679bfa097c86 Mon Sep 17 00:00:00 2001 From: Evgeniy Unzhakov Date: Fri, 2 Oct 2026 07:13:14 +0300 Subject: [PATCH] refactor(cli): delegate paceENSDF config building to library Remove the duplicated inline paceENSDF config builder in the CLI. It renormalized branch intensities before filtering, diverging from the library decay_info_to_config path that library callers use (audit finding D5). CLI paceENSDF configs now come from create_config_from_source exactly once per run. Regression test spies on create_config_from_source during a CLI run. --- beta_spectrum/cli.py | 70 +++++------------------------ tests/integration/test_cli_flows.py | 27 +++++++++++ 2 files changed, 38 insertions(+), 59 deletions(-) diff --git a/beta_spectrum/cli.py b/beta_spectrum/cli.py index a54604a..a008666 100644 --- a/beta_spectrum/cli.py +++ b/beta_spectrum/cli.py @@ -178,15 +178,10 @@ def _run(args: argparse.Namespace) -> None: from beta_spectrum.logging_utils import get_logger from beta_spectrum.nuclear_data import ( create_config_from_source, - get_decay_info_from_paceENSDF, json_to_config, load_json_input, ) - from beta_spectrum.spectrum import ( - BetaSpectrum, - BranchConfig, - SpectrumConfig, - ) + from beta_spectrum.spectrum import BetaSpectrum from beta_spectrum.output import BetaSpectrumAnalyzer # Determine logging level @@ -246,62 +241,19 @@ def _run(args: argparse.Namespace) -> None: csv_separator = "," if args.csv_separator is None else args.csv_separator normalize_to_one = bool(args.normalize_to_one) - # Build config + # Build config — a single delegated path for every data source. config: SpectrumConfig if source_type == "paceENSDF": logger.info("Source: paceENSDF nuclide=%s", source_value) - decay_info = get_decay_info_from_paceENSDF(source_value, "beta_minus") - - # Normalize branch intensities to sum to 1.0 - if decay_info.branches: - total_intensity = sum(b.intensity for b in decay_info.branches) - if total_intensity > 0: - branches = [ - BranchConfig( - endpoint_MeV=( - (decay_info.endpoint_MeV - b.level_energy_keV / 1000.0) - if b.level_energy_keV > 0 - else decay_info.endpoint_MeV - ), - transition_type=b.transition_type, - intensity=b.intensity / total_intensity, - ) - for b in decay_info.branches - if b.intensity / total_intensity >= intensity_cutoff - ] - config = SpectrumConfig( - Z_parent=decay_info.Z_parent, - Z_daughter=decay_info.Z_daughter, - A_number=decay_info.A_number, - endpoint_MeV=decay_info.endpoint_MeV, - transition_type=decay_info.transition_type, - e_step_MeV=e_step_MeV, - neutrino_mass_MeV=neutrino_mass_MeV, - branches=branches, - intensity_cutoff=intensity_cutoff, - csv_separator=csv_separator, - normalize_to_one=normalize_to_one, - ) - else: - config = create_config_from_source( - "paceENSDF", - nuclide=source_value, - e_step_MeV=e_step_MeV, - intensity_cutoff=intensity_cutoff, - neutrino_mass_MeV=neutrino_mass_MeV, - csv_separator=csv_separator, - normalize_to_one=normalize_to_one, - ) - else: - config = create_config_from_source( - "paceENSDF", - nuclide=source_value, - e_step_MeV=e_step_MeV, - intensity_cutoff=intensity_cutoff, - neutrino_mass_MeV=neutrino_mass_MeV, - csv_separator=csv_separator, - normalize_to_one=normalize_to_one, - ) + config = create_config_from_source( + "paceENSDF", + nuclide=source_value, + e_step_MeV=e_step_MeV, + intensity_cutoff=intensity_cutoff, + neutrino_mass_MeV=neutrino_mass_MeV, + csv_separator=csv_separator, + normalize_to_one=normalize_to_one, + ) else: logger.info("Source: JSON file=%s", source_value) data = load_json_input(source_value) diff --git a/tests/integration/test_cli_flows.py b/tests/integration/test_cli_flows.py index 382963e..8b08a66 100644 --- a/tests/integration/test_cli_flows.py +++ b/tests/integration/test_cli_flows.py @@ -162,3 +162,30 @@ class TestCLIPaceENSDF: main(["Tc99", "--output", out, "--neutrino-mass", "0.35"]) assert exc.value.code == 1 assert not (tmp_path / "tc99deg.csv").exists() + + def test_cli_builds_paceensdf_config_via_library( + self, monkeypatch: Any, capsys: Any + ) -> None: + """CLI must delegate paceENSDF config building to the library + (create_config_from_source), not duplicate it inline (D5).""" + import beta_spectrum.nuclear_data as nuclear_data + + real_builder = nuclear_data.create_config_from_source + calls: List[Dict[str, Any]] = [] + + def spy(source: str, **kwargs: Any) -> Any: + calls.append({"source": source, **kwargs}) + return real_builder(source, **kwargs) + + monkeypatch.setattr(nuclear_data, "create_config_from_source", spy) + + main(["Cs137", "--dry-run", "--intensity-cutoff", "0.9"]) + capsys.readouterr() + + assert len(calls) == 1, ( + "CLI must delegate paceENSDF config building to the library " + "builder exactly once" + ) + assert calls[0]["source"] == "paceENSDF" + assert calls[0]["nuclide"] == "Cs137" + assert calls[0]["intensity_cutoff"] == 0.9