Skip to content

feat(integration): Phase 3.1 commit 5 — rank_alternatives service + lifecycle#69

Closed
Artic0din wants to merge 2 commits into
phase-3-1-coordinator-hookfrom
phase-3-1-rank-service
Closed

feat(integration): Phase 3.1 commit 5 — rank_alternatives service + lifecycle#69
Artic0din wants to merge 2 commits into
phase-3-1-coordinator-hookfrom
phase-3-1-rank-service

Conversation

@Artic0din
Copy link
Copy Markdown
Owner

@Artic0din Artic0din commented May 16, 2026

Summary

Commit 5 of Phase 3.1. Stacks on #68 (coordinator hook). Wires the daily ranking job into integration setup/teardown and adds a manual-trigger HA service.

What was broken

Commit 4 (#68) added coordinator.schedule_daily_ranking() and async_run_ranking_job() but nothing called them. The methods existed; the lifecycle didn't invoke them. Equally, there was no way for a user to force-run the ranking job after a plan switch — they'd have to wait until the next 00:30 fire.

What this fixes

  1. async_setup_entry now invokes schedule_daily_ranking() + a one-shot async_create_task(async_run_ranking_job()) so the alternatives data populates immediately on fresh install rather than being empty until midnight.
  2. async_unload_entry calls cancel_ranking() mirroring the existing cancel_persist() pattern.
  3. New HA service pricehawk.rank_alternatives lets users force-run the pipeline from Developer Tools → Services. Single top_k field (1-100, default 20) clamped server-side.
  4. Service unregistered on last-entry teardown alongside the existing two.

Test plan

  • ruff check — clean
  • python -c "import yaml; yaml.safe_load(...)" services.yaml validates
  • pytest -q — 713/713 full suite still passes (no regression)
  • Live smoke test: install integration, check Developer Tools shows the new service, call it, verify _LOGGER shows "rank_alternatives service: ran successfully, N result(s)" — deferred to user runtime
  • Restart HA, verify 00:30 callback fires (will be visible in log) — deferred

Changes

File Δ Purpose
custom_components/pricehawk/__init__.py +25 Lifecycle wiring + service handler
custom_components/pricehawk/services.yaml +22 Service declaration

Service shape

rank_alternatives:
  fields:
    top_k:
      required: false
      default: 20
      selector:
        number: {min: 1, max: 100, mode: slider}

Lifecycle additions

# async_setup_entry, after schedule_persist():
coordinator.schedule_daily_ranking()
hass.async_create_task(coordinator.async_run_ranking_job())

# async_unload_entry, before async_persist_state():
coordinator.cancel_ranking()

Why

No tests for the service handler itself: it's a closure inside async_setup_entry (tightly coupled to HA's hass.services.async_register plumbing — can't unit-test without HA itself). The underlying async_run_ranking_job and its pure-logic backing (ranking_job.run_ranking_job) are already covered by 17 tests in #68. The handler is just arg coercion + delegation.

Breaking Changes

None. Additive — no existing service changed, no existing lifecycle method touched.

Files Changed

  • custom_components/pricehawk/__init__.py (+25)
  • custom_components/pricehawk/services.yaml (+22)

Stacked on

Depends on #68 — must merge that PR first or this PR's diff will look messy after rebase. Open this PR against phase-3-1-coordinator-hook (#68's branch) so the diff stays clean. Will retarget to dev after #68 merges.

🤖 Generated with Claude Code

Summary by Sourcery

Integrate the daily alternatives ranking job into the PriceHawk integration lifecycle and expose a manual trigger service in Home Assistant.

New Features:

  • Add automatic scheduling and initial execution of the daily alternatives ranking job during integration setup.
  • Introduce a Home Assistant service pricehawk.rank_alternatives to manually trigger the alternatives ranking pipeline with a configurable top_k parameter.

Enhancements:

  • Ensure ranking tasks are cancelled and the rank_alternatives service is unregistered when the integration is unloaded to align with existing lifecycle cleanup.

Summary

Changes

  • Integrated daily ranking job into lifecycle: async_setup_entry now calls coordinator.schedule_daily_ranking() and immediately triggers a ranking run via hass.async_create_task(coordinator.async_run_ranking_job()) to populate alternatives data on fresh install.

  • Added cleanup on unload: async_unload_entry now calls coordinator.cancel_ranking() to mirror existing cancel_persist() cleanup pattern.

  • Exposed manual ranking service: New rank_alternatives Home Assistant service allows triggering the ranking job manually. The service accepts top_k parameter (integer, range 1–100, default 20) which is clamped server-side and passed to coordinator.async_run_ranking_job(top_k=...).

  • Service lifecycle management: The rank_alternatives service is automatically unregistered during last-entry teardown alongside existing services.

Why

This change completes the integration of the alternatives ranking pipeline into the Home Assistant lifecycle, ensuring:

  • Fresh installs have ranked alternatives data available immediately
  • Proper resource cleanup when entries are unloaded
  • Users can manually trigger ranking without restarting Home Assistant
  • Customizable result limits for different use cases

Breaking Changes

None. All changes are additive with no modifications to existing APIs or function signatures.

Files Changed

File Lines Added Lines Removed
custom_components/pricehawk/init.py 26 0
custom_components/pricehawk/services.yaml 21 0
Total 47 0

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 16, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (3)
  • main
  • develop
  • dev

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1871897e-deab-4fec-a115-ac48b60d8a45

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch phase-3-1-rank-service
  • 🛠️ scrub-secrets
  • 🛠️ no-hardcoded-rates
  • 🛠️ amber-api-limits
  • 🛠️ dashboard-protocol-safety

Comment @coderabbitai help to get the list of available commands and usage tips.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 16, 2026

Reviewer's Guide

Wires the daily ranking job into the integration lifecycle and exposes a new Home Assistant service to trigger ranking on demand, plus matching teardown/unregister logic.

File-Level Changes

Change Details Files
Wire daily ranking job into setup so rankings are scheduled and run immediately on install, and cancelled on unload.
  • Call coordinator.schedule_daily_ranking() during async_setup_entry after schedule_persist() to schedule the 00:30 ranking job.
  • Create a background task via hass.async_create_task(coordinator.async_run_ranking_job()) so the initial ranking runs immediately on setup.
  • Invoke coordinator.cancel_ranking() in async_unload_entry alongside cancel_persist() to stop scheduled ranking when the entry is unloaded.
custom_components/pricehawk/__init__.py
Add a manual rank_alternatives Home Assistant service and ensure it is registered/unregistered with the integration lifecycle.
  • Define handle_rank_alternatives() service handler that reads an optional top_k parameter (default 20), clamps it between 1 and 100, and calls coordinator.async_run_ranking_job(top_k=top_k).
  • Log the number of results returned from the ranking job for observability.
  • Register the rank_alternatives service with hass.services.async_register() during setup and remove it in async_unload_entry when the last entry is unloaded.
  • Declare the rank_alternatives service in services.yaml with a top_k numeric field (1–100 slider, default 20) and descriptive metadata.
custom_components/pricehawk/__init__.py
custom_components/pricehawk/services.yaml

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • The service handler handle_rank_alternatives currently does a blind int(call.data.get("top_k", 20)); consider validating/coercing the value more defensively (e.g., handle ValueError and non-numeric input) so a malformed call doesn’t raise unexpectedly.
  • You type call as object in the new service handler; using the concrete homeassistant.core.ServiceCall type here (as done elsewhere in HA code) would make the interface clearer and help static analysis.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The service handler `handle_rank_alternatives` currently does a blind `int(call.data.get("top_k", 20))`; consider validating/coercing the value more defensively (e.g., handle `ValueError` and non-numeric input) so a malformed call doesn’t raise unexpectedly.
- You type `call` as `object` in the new service handler; using the concrete `homeassistant.core.ServiceCall` type here (as done elsewhere in HA code) would make the interface clearer and help static analysis.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@custom_components/pricehawk/__init__.py`:
- Around line 193-195: handle_rank_alternatives currently casts
call.data.get("top_k", 20) to int directly which can raise ValueError/TypeError
for malformed payloads; validate and sanitize the raw value from call.data
before casting (e.g., check for None, numeric string, or int), perform a safe
int conversion within a try/except that catches ValueError and TypeError, and on
error fall back to the default (20) or return a clear service error; ensure the
bounded logic (max/min) still uses the sanitized integer and reference the
handle_rank_alternatives function, the top_k variable and call.data when making
the fix.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 38ab215f-fb32-4dca-a376-57f3a174a08d

📥 Commits

Reviewing files that changed from the base of the PR and between bfc89a1 and 1aff3eb.

📒 Files selected for processing (2)
  • custom_components/pricehawk/__init__.py
  • custom_components/pricehawk/services.yaml
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Use async/await for all I/O operations
NEVER hardcode tokens, API keys, or credentials in any file — use HA config entry storage
State restore MUST validate storage version before loading
from_dict() methods MUST receive an explicit HA-timezone date — no date.today() fallback

Files:

  • custom_components/pricehawk/__init__.py

⚙️ CodeRabbit configuration file

**/*.py: Check for: type hints on all public functions, no bare except:, SQL injection risks, missing input sanitisation, secrets not in code, Flask Blueprint structure respected, APScheduler job error handling.

Files:

  • custom_components/pricehawk/__init__.py
🔇 Additional comments (2)
custom_components/pricehawk/services.yaml (1)

34-53: LGTM!

custom_components/pricehawk/__init__.py (1)

44-49: LGTM!

Also applies to: 218-227

Comment thread custom_components/pricehawk/__init__.py
Artic0din added a commit that referenced this pull request May 16, 2026
CR found that ``int(call.data.get("top_k", 20))`` raises
ValueError/TypeError on malformed service data (e.g. a YAML typo
sending ``top_k: "abc"``). The exception propagates back to the
service caller and fails the run instead of degrading gracefully.

Now wraps the coercion in try/except, logs a warning with the bad
value, and falls back to the default 20. Clamp [1, 100] still
applied after.

Same kind of defence as elsewhere in the codebase (``safe_int``,
``safe_decimal`` for incentive parsers).

Tests: 714/714 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Artic0din Artic0din force-pushed the phase-3-1-rank-service branch from 1aff3eb to 6f1f923 Compare May 16, 2026 22:50
Artic0din and others added 2 commits May 17, 2026 09:12
…+ lifecycle

Wires the ranking job into the integration's lifecycle and exposes
a manual-trigger HA service. Completes Phase 3.1's user-visible
plumbing (sensor + dashboard exposure is commit 6).

Lifecycle wiring
----------------

- ``async_setup_entry`` now calls ``coordinator.schedule_daily_ranking()``
  to register the 00:30 local-time callback, plus ``hass.async_create_task``
  for an immediate first run so the alternatives data isn't empty until
  midnight on a fresh install.
- ``async_unload_entry`` calls ``coordinator.cancel_ranking()`` mirroring
  the existing ``cancel_persist()`` pattern.

HA service: pricehawk.rank_alternatives
---------------------------------------

- ``services.yaml`` declares the service with a single ``top_k`` int field
  (1-100, default 20).
- Handler clamps top_k to [1, 100] and delegates to
  ``coordinator.async_run_ranking_job(top_k=...)``.
- Most useful right after a plan switch: lets the user force-run the
  ranking pipeline so the alternatives sensor reflects the new
  distributor / postcode without waiting for the next 00:30 schedule.
- Unregistered alongside the other two services in
  ``async_unload_entry`` when the last config entry goes away.

No tests
--------

The service handler is a closure inside ``async_setup_entry`` (tightly
coupled to HA's service registration plumbing — can't be unit-tested
without HA itself). The underlying ``async_run_ranking_job`` is already
covered by ``tests/test_coordinator_ranking.py`` (17 tests via the
``ranking_job`` module). Live smoke test will validate the wiring once
the integration runs in HA.

Full suite: 713/713 still passing. Ruff clean. services.yaml validates.

Refs PHASE-3-ROADMAP.md §3.1. Next (optional commit 6): sensor for
ranked alternatives + dashboard exposure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CR found that ``int(call.data.get("top_k", 20))`` raises
ValueError/TypeError on malformed service data (e.g. a YAML typo
sending ``top_k: "abc"``). The exception propagates back to the
service caller and fails the run instead of degrading gracefully.

Now wraps the coercion in try/except, logs a warning with the bad
value, and falls back to the default 20. Clamp [1, 100] still
applied after.

Same kind of defence as elsewhere in the codebase (``safe_int``,
``safe_decimal`` for incentive parsers).

Tests: 714/714 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Artic0din Artic0din force-pushed the phase-3-1-rank-service branch from 6f1f923 to d022187 Compare May 16, 2026 23:12
@Artic0din Artic0din deleted the branch phase-3-1-coordinator-hook May 17, 2026 02:02
@Artic0din Artic0din closed this May 17, 2026
Artic0din added a commit that referenced this pull request May 17, 2026
…ifecycle (#72)

* feat(coordinator): Phase 3.1 commit 4 — daily ranking job hook

Wires the multi-plan ranking pipeline (from PR #64) into the
coordinator on a daily 00:30 local schedule. Cheap-rank only for
now; deep-rank (consumption replay) joins in Phase 3.2 when the
universal HA-history backfill ships.

Architecture
------------

Pure logic in new ``custom_components/pricehawk/cdr/ranking_job.py``:

- ``get_user_geography(options)`` — extracts ``(state, postcode,
  distributor)`` from a config_entry's options. ``distributor``
  comes from the user's current cdr_plan's
  ``geography.distributors[0]`` (the plan the user already accepted,
  so its distributor is by definition theirs).

- ``get_competitor_retailers(session, options, *,
  competitor_fragments)`` — returns the retailer list scanned daily.
  Composition: user's CURRENT retailer (via ``cdr_plan.data.brand``)
  + hardcoded big-4 nationally-active competitors (AGL, Origin,
  EnergyAustralia, Red Energy). Dedup by brand_id so the user's
  retailer isn't double-scanned when it's also a big-4. Custom
  fragment tuple supported for tests + future user-configurable list.

- ``run_ranking_job(session, options, *, top_k, plan_cache,
  competitor_fragments)`` — top-level orchestrator. Returns the
  cheap-ranked top-K plans.

Coordinator-side wrappers
-------------------------

- New attributes on PriceHawkCoordinator:
  - ``_cheap_ranked_alternatives: list[dict]``
  - ``_ranking_last_run_at: datetime | None``
  - ``_ranking_plan_cache: dict[str, dict]``
  - ``_ranking_unsub: CALLBACK_TYPE | None``

- ``schedule_daily_ranking()`` registers ``async_track_time_change``
  for ``hour=0, minute=30`` local. Safe to call twice (second call
  replaces first).
- ``cancel_ranking()`` clears the handle.
- ``async_run_ranking_job(*, top_k)`` is a thin delegate around
  ``ranking_job.run_ranking_job``. Owns HA-side concerns: session
  acquisition, exception swallowing across the daily boundary,
  state persistence, cache reset. Empty-result runs keep prior
  ranking (transient failure → don't zero yesterday's data).

Why a separate module
---------------------

PriceHawkCoordinator inherits from
``DataUpdateCoordinator[dict[str, Any]]`` which gets mocked away by
``tests/conftest.py``. The class is therefore unreachable in unit
tests. Extracting the pure logic to ``ranking_job`` keeps it
unit-testable while the class methods stay as thin delegates.

Tests
-----

17 new tests in ``tests/test_coordinator_ranking.py``:

- Module constants (competitor fragments, run-time).
- ``get_user_geography``: 6 cases (happy path, missing fields, list
  ordering, empty options).
- ``get_competitor_retailers``: 5 cases (current retailer first,
  dedup, missing brand, unmatchable fragment, custom override).
- ``run_ranking_job``: 4 cases (empty retailers, geography
  forwarding, cache passthrough, top_k forwarding).

Also added ``homeassistant.helpers.aiohttp_client`` to ``conftest.py``
mock list — coordinator.py imports it; without the mock, importing
the coordinator silently fell back to a MagicMock class.

Full suite: 713/713 pass (was 696 — +17 new). Ruff clean.

Refs PHASE-3-ROADMAP.md §3.1. Next commit: HA service
``pricehawk.rank_alternatives`` in ``__init__.py``.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: PR #68 CR findings — cache TTL + distributors type guard

Two findings from CodeRabbit and Sourcery on PR #68 (coordinator hook).

cache TTL (Sourcery + CR Major, same issue):
- coordinator.py — replaced unconditional ``cache.clear()`` at end of
  ``async_run_ranking_job`` with a date-rollover check at the start.
  Same-day reruns (scheduled at 00:30, manual via rank_alternatives
  service) now reuse cached plans as the docstring promised. New
  local day → cache clears before the run so overnight republished
  plans get fresh data.
- New ``_ranking_cache_date: date | None`` attribute tracks the
  cache's "as-of" day.

distributors type guard (CR Minor):
- ranking_job.py — ``get_user_geography`` now type-guards
  ``distributors`` with ``isinstance(..., list)`` before indexing.
  Malformed CDR payload could ship distributors as a string (e.g.
  ``"United Energy"``) and ``[0]`` would silently become ``"U"`` —
  skewing the ranking filter without raising.

Tests: 18/18 ranking tests pass (was 17 + 1 new for type guard).
Full suite 714/714.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: PR #68 round-2 CR — nested shape guards + asyncio.Lock

Three CodeRabbit findings on the round-1 fixes (f998d16):

1. ranking_job.py — extracted ``_safe_plan_data(options)`` helper
   that walks ``cdr_plan["data"]`` with isinstance gates at every
   level. Previously ``cdr_plan["data"]`` was assumed to be a dict;
   a malformed payload could ship it as a string (e.g.
   ``cdr_plan = {"data": "broken"}``) and trigger AttributeError on
   ``.get("geography")``. Same for ``brand`` — now ``isinstance(...,
   str)`` before ``.strip()``. ``geography`` non-dict also guarded
   in ``get_user_geography``. First-distributor-must-be-str check
   added to prevent dict / int from sneaking through as a
   distributor filter value.

2. coordinator.py — added ``self._ranking_lock = asyncio.Lock()`` to
   serialise concurrent runs. Scheduled 00:30 callback + manual
   service trigger could enter ``async_run_ranking_job``
   simultaneously, interleaving ``_ranking_plan_cache`` mutations
   and duplicating every expensive CDR detail fetch. The lock means
   the second caller blocks briefly then returns the
   freshly-populated cache results.

3. tests/test_coordinator_ranking.py — added 4 new tests for
   non-dict ``cdr_plan``, non-dict ``data``, non-dict ``geography``,
   and non-string first distributor. Covers the AttributeError
   regression paths CR flagged.

Tests: 22/22 ranking + 722/722 full suite green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(integration): Phase 3.1 commit 5 — rank_alternatives HA service + lifecycle

Wires the ranking job into the integration's lifecycle and exposes
a manual-trigger HA service. Completes Phase 3.1's user-visible
plumbing (sensor + dashboard exposure is commit 6).

Lifecycle wiring
----------------

- ``async_setup_entry`` now calls ``coordinator.schedule_daily_ranking()``
  to register the 00:30 local-time callback, plus ``hass.async_create_task``
  for an immediate first run so the alternatives data isn't empty until
  midnight on a fresh install.
- ``async_unload_entry`` calls ``coordinator.cancel_ranking()`` mirroring
  the existing ``cancel_persist()`` pattern.

HA service: pricehawk.rank_alternatives
---------------------------------------

- ``services.yaml`` declares the service with a single ``top_k`` int field
  (1-100, default 20).
- Handler clamps top_k to [1, 100] and delegates to
  ``coordinator.async_run_ranking_job(top_k=...)``.
- Most useful right after a plan switch: lets the user force-run the
  ranking pipeline so the alternatives sensor reflects the new
  distributor / postcode without waiting for the next 00:30 schedule.
- Unregistered alongside the other two services in
  ``async_unload_entry`` when the last config entry goes away.

No tests
--------

The service handler is a closure inside ``async_setup_entry`` (tightly
coupled to HA's service registration plumbing — can't be unit-tested
without HA itself). The underlying ``async_run_ranking_job`` is already
covered by ``tests/test_coordinator_ranking.py`` (17 tests via the
``ranking_job`` module). Live smoke test will validate the wiring once
the integration runs in HA.

Full suite: 713/713 still passing. Ruff clean. services.yaml validates.

Refs PHASE-3-ROADMAP.md §3.1. Next (optional commit 6): sensor for
ranked alternatives + dashboard exposure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: PR #69 CR finding — handle invalid top_k payload defensively

CR found that ``int(call.data.get("top_k", 20))`` raises
ValueError/TypeError on malformed service data (e.g. a YAML typo
sending ``top_k: "abc"``). The exception propagates back to the
service caller and fails the run instead of degrading gracefully.

Now wraps the coercion in try/except, logs a warning with the bad
value, and falls back to the default 20. Clamp [1, 100] still
applied after.

Same kind of defence as elsewhere in the codebase (``safe_int``,
``safe_decimal`` for incentive parsers).

Tests: 714/714 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Artic0din Artic0din deleted the phase-3-1-rank-service branch May 17, 2026 02:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant