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.
This commit is contained in:
2026-10-02 07:13:14 +03:00
parent d166532779
commit 9c3dd80b5c
2 changed files with 38 additions and 59 deletions
+2 -50
View File
@@ -178,15 +178,10 @@ def _run(args: argparse.Namespace) -> None:
from beta_spectrum.logging_utils import get_logger from beta_spectrum.logging_utils import get_logger
from beta_spectrum.nuclear_data import ( from beta_spectrum.nuclear_data import (
create_config_from_source, create_config_from_source,
get_decay_info_from_paceENSDF,
json_to_config, json_to_config,
load_json_input, load_json_input,
) )
from beta_spectrum.spectrum import ( from beta_spectrum.spectrum import BetaSpectrum
BetaSpectrum,
BranchConfig,
SpectrumConfig,
)
from beta_spectrum.output import BetaSpectrumAnalyzer from beta_spectrum.output import BetaSpectrumAnalyzer
# Determine logging level # Determine logging level
@@ -246,53 +241,10 @@ def _run(args: argparse.Namespace) -> None:
csv_separator = "," if args.csv_separator is None else args.csv_separator csv_separator = "," if args.csv_separator is None else args.csv_separator
normalize_to_one = bool(args.normalize_to_one) normalize_to_one = bool(args.normalize_to_one)
# Build config # Build config — a single delegated path for every data source.
config: SpectrumConfig config: SpectrumConfig
if source_type == "paceENSDF": if source_type == "paceENSDF":
logger.info("Source: paceENSDF nuclide=%s", source_value) 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( config = create_config_from_source(
"paceENSDF", "paceENSDF",
nuclide=source_value, nuclide=source_value,
+27
View File
@@ -162,3 +162,30 @@ class TestCLIPaceENSDF:
main(["Tc99", "--output", out, "--neutrino-mass", "0.35"]) main(["Tc99", "--output", out, "--neutrino-mass", "0.35"])
assert exc.value.code == 1 assert exc.value.code == 1
assert not (tmp_path / "tc99deg.csv").exists() 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