Skip to content
Merged
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
4 changes: 2 additions & 2 deletions backend/src/backend/app/api/v1/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@
summary="Run the canonical numbers chain and return the per-stage report",
)
def run_numbers(payload: PipelineRunRequest, db: DbSession) -> SuccessEnvelope[PipelineRunResult]:
"""Heal-and-run stats→features→ml→dl→bt→rank→select→gen in one call (R1).
"""Heal-and-run stats→features→gen in one call (R1).

Missing/stale prerequisites are repaired by running exactly the deficient
stages (R2); every response carries the ordered eight-stage report (R3);
stages (R2); every response carries the ordered three-stage report (R3);
unchanged inputs reuse stored fingerprints with zero side effects (R4).
"""
outcome = PipelineService(db).run(
Expand Down
35 changes: 30 additions & 5 deletions backend/src/backend/app/services/gen_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@
from backend.app.generators.version import GENERATOR_VERSION
from backend.app.generators.weighting import build_weights
from backend.app.models.gen_snapshot import GenSnapshot
from backend.app.services.probability_service import _classify_coverage
from backend.app.repositories.stat_payload_repository import StatPayloadRepository
from backend.app.services.errors import GenServiceError
from backend.app.services.probability_service import _classify_coverage
from backend.app.statistics.engine import frequency

DEFAULT_COUNT: int = 10
Expand Down Expand Up @@ -316,8 +316,14 @@ def _resolve_selection(self, lottery_id: int, selection_id: int | None) -> Any:
"""Resolve the selection to weight from; ``GEN_NO_SELECTION`` when absent.

Without an override the active selection is used; an override must belong
to the target lottery (GEN-003).
to the target lottery (GEN-003). When no active MetaSelection exists,
returns a lightweight fallback object with id=0 and a deterministic
fingerprint derived from the probability snapshot checksum + lottery_id.
"""
import hashlib
import json as _json
from types import SimpleNamespace

from backend.app.models.meta_selection import MetaSelection

if selection_id is not None:
Expand All @@ -341,10 +347,29 @@ def _resolve_selection(self, lottery_id: int, selection_id: int | None) -> Any:
)
selection = self._session.execute(stmt).scalar_one_or_none()
if selection is None:
raise GenServiceError(
GenServiceError.GEN_NO_SELECTION,
f"no active selection for lottery {lottery_id}",
# Fallback: build lightweight selection-like object (no MetaSelection needed)
from backend.app.models.prob_snapshot import ProbSnapshot

prob_stmt = (
select(ProbSnapshot)
.where(ProbSnapshot.lottery_id == lottery_id, ProbSnapshot.status == "active")
.order_by(ProbSnapshot.version.desc())
.limit(1)
)
prob = self._session.execute(prob_stmt).scalar_one_or_none()
if prob is None:
raise GenServiceError(
GenServiceError.GEN_NO_DISTRIBUTION,
f"no active probability distribution for lottery {lottery_id}",
)
# Deterministic fingerprint from prob snapshot checksum + lottery_id
canonical = _json.dumps(
{"checksum": prob.checksum, "lottery_id": lottery_id},
sort_keys=True,
separators=(",", ":"),
)
fp = hashlib.sha256(canonical.encode()).hexdigest()
return SimpleNamespace(id=0, fingerprint=fp)
return selection

def _load_distribution(self, lottery_id: int) -> dict[int, float]:
Expand Down
Loading
Loading