Skip to content

Feature/api store currency locale - #211

Merged
jez500 merged 6 commits into
mainfrom
feature/api-store-currency-locale
Aug 8, 2026
Merged

Feature/api store currency locale#211
jez500 merged 6 commits into
mainfrom
feature/api-store-currency-locale

Conversation

@jez500

@jez500 jez500 commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Added optional AI-assisted healing for metadata extraction, with status and reason details in responses.
    • Added configurable extraction time limits and AI provider availability handling.
    • Added client configuration details for supported capabilities and extraction limits.
    • Improved validation for scrape strategy values.
    • Improved deal scoring for limited price history, including unknown verdicts and competitive pricing.
    • Added dynamic product insight styling and messaging based on deal verdicts.
  • Documentation

    • Documented healing behavior, time limits, response statuses, capabilities, and validation rules.

jez500 added 3 commits August 8, 2026 12:29
A schema_org strategy could be tested but not saved. meta-extraction requires
`value` to be null for schema_org and non-empty for every other type, while the
write endpoints required it unconditionally (create) or whenever a type was
present (update). The extension's Tune tab defaults all three fields to
schema_org and omits `value`, so "Test all" succeeded and "Save to store" 422'd
on the identical strategy. No single payload satisfied both, so any store whose
fields are detected via schema.org could not be saved at all.

Extracts the meta-extraction closure into App\Rules\ScrapeStrategyValue and
points all three requests at it. The rule locates the sibling `type` relative to
the attribute under validation, so it works at any path without a configured
prefix, and is implicit so it still runs when `value` is absent - which is both
how a schema_org strategy is legitimately sent and how a selector with no
expression is caught. Which types need a value stays owned by
ScraperStrategyType::needsValue().

A saved schema_org strategy round-trips as `{"type": "schema_org"}` with no
value key at all - StandardStrategyDto drops null values rather than persisting
them - and that shape is valid input to all three endpoints. Covered by a test
that creates a store, reads it back and posts it to meta-extraction.

Also adds the stores_filter_domain capability to /api/client-config, so clients
stop inferring filter[domain] support from products_filter_url.
POST /api/meta-extraction is synchronous and user-facing but had no ceiling. The
scrape was bounded at 10s; the healing it can trigger was bounded only by the
provider's own timeout (120s on this instance), so one request could block for
over two minutes. Worse, healing is reached from the auto-create path - a URL
matching no store - which is the first thing a user hits on a new retailer.

Verified against a wedged provider: the same request went from HTTP 200 after
120.1s to HTTP 200 after 0.08s by default, and 24.1s with heal=true.

Healing is now opt-in via a `heal` boolean on the request, defaulting to false.
A caller testing selectors it just typed wants a fast deterministic answer, and
healing may propose a different strategy than the one under test; "work it out
for me" is a separate, explicitly slow action.

Every request also carries a wall-clock budget (config
price_buddy.meta_extraction.budget_seconds, default 25) spanning the scrape, any
browser re-scrape and the AI call, which is handed the time remaining instead of
the provider's timeout. Healing is declined outright below heal_floor_seconds
rather than started and cut off. The agent's own tool fetches draw from the same
budget and throw once it is gone - they run in-process between model calls, so
the model call timeout does not cover them.

Healing never fails the request: every failure mode returns 200 with the
deterministic result and a `healing` object saying what happened
(attempted/applied/reason, reason being disabled|not_needed|timeout|error), so a
client can explain a thin result instead of showing empty fields.

/api/client-config gains limits.meta_extraction_timeout_seconds so clients set
their abort from the instance's real ceiling rather than a hardcoded guess.

The admin UI heal path is unaffected - it calls previewForUrl() without a budget,
which keeps the provider timeout, the right ceiling when a human chose to wait.

BREAKING: API callers that relied on healing running by default must now send
heal: true. The four API tests that exercised healing were updated accordingly.
Bounding the meta-extraction request stopped a wedged provider costing 120s, but
it still cost the full 25s budget on every single request. A provider that has
failed the last N calls will almost certainly fail the next one, and finding out
is the expensive part.

AiProviderHealth tracks consecutive failures per provider id in the cache. After
failure_threshold consecutive failures (default 3) the provider is treated as
unavailable for cooldown_seconds (default 300), which doubles as the counting
window so thinly spread failures never accumulate. Any success closes it
immediately.

AiService feeds it, since that is the only layer that knows whether a provider
call succeeded. The Ollama reachability check deliberately does not close the
breaker: listing models proves the HTTP front end is up, not that generation
works, which is exactly the state this instance is in.

Only callers on a deadline consult it. The meta-extraction endpoint skips healing
with the new reason `provider_unavailable`; the admin UI heal button and queued
healing keep exercising the provider, so they act as the probe that notices
recovery rather than waiting out the cooldown.

Verified live against the wedged provider: three requests at 24.1s each, then the
fourth returned in 0.105s with reason `provider_unavailable`.

Note for API clients: `provider_unavailable` is a new value in the `healing.reason`
set. It means the instance is degraded rather than the page being unextractable,
so the same URL is worth retrying later.
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds opt-in AI healing for metadata extraction, with shared request budgets, healing outcome metadata, and AI provider circuit breaking. It also centralizes scrape-strategy validation and updates product deal scoring and presentation.

Changes

Meta extraction and validation

Layer / File(s) Summary
Shared scrape-strategy validation
app/Rules/ScrapeStrategyValue.php, app/Filament/Resources/StoreResource/Api/Requests/*, app/Http/Requests/MetaExtractionRequest.php, tests/Feature/Api/StoreScrapeStrategyValidationTest.php
Strategy values are validated from sibling strategy types. schema_org strategies omit values. Other strategies require non-empty strings.
Healing outcomes and extraction budget
app/Dto/HealingOutcomeDto.php, app/Enums/HealingReason.php, app/Services/ExtractionBudget.php, app/Services/Ai/HealingContext.php, tests/Unit/Services/ExtractionBudgetTest.php
Healing states track attempted, applied, and reason values. Shared budgets provide remaining-time checks and request timeouts.
AI provider health tracking
app/Services/Ai/AiProviderHealth.php, app/Services/AiService.php, tests/Unit/Services/AiProviderHealthTest.php, tests/Feature/Services/AiServiceHealthTest.php
Provider failures open cache-backed breakers after a configured threshold. Successful generation clears provider failure state.
Budget-aware healing orchestration
app/Services/MetaExtractionService.php, app/Services/AiConfigHealer.php, config/price_buddy.php, tests/Feature/Api/MetaExtractionBudgetTest.php
Healing uses the remaining extraction budget, skips unavailable or insufficient-budget cases, classifies failures, and preserves deterministic extraction results.
API healing and configuration surface
app/Http/Controllers/Api/MetaExtractionController.php, app/Http/Resources/MetaExtractionResource.php, app/Http/Controllers/Api/ClientConfigController.php, docs/docs/api.md, tests/Feature/Api/ClientConfigApiTest.php
The API accepts heal, always returns healing metadata, and publishes extraction limits and capabilities.

Product insights

Layer / File(s) Summary
Deal scoring without price variation
app/Services/Insights/DealScoreCalculator.php, app/Services/Insights/ProductInsights.php, tests/Unit/Services/Insights/*
Prices without meaningful variation receive an unknown result unless they beat another listing, in which case they receive a low-confidence great result.
Verdict-specific insights presentation
resources/views/components/product-badges.blade.php, resources/views/filament/pages/product/insights/index.blade.php, tests/Feature/Filament/ProductInsightsTabTest.php
Insight badges and hero styling now handle unknown verdicts and use dynamic verdict palette accents.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant MetaExtractionController
  participant MetaExtractionService
  participant AiConfigHealer
  participant AiService
  Client->>MetaExtractionController: Submit extraction with heal=true
  MetaExtractionController->>MetaExtractionService: Start extraction with shared budget
  MetaExtractionService->>AiConfigHealer: Request budget-aware healing
  AiConfigHealer->>AiService: Run provider call with remaining timeout
  AiService-->>AiConfigHealer: Return result or failure
  AiConfigHealer-->>MetaExtractionService: Return healed configuration or no result
  MetaExtractionService-->>MetaExtractionController: Return extraction and healing outcome
  MetaExtractionController-->>Client: Serialize API response
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title describes store currency and locale changes, but the pull request adds healing budgets, validation, provider health, and product insight behavior. Rename the pull request to summarize the primary changes, such as adding bounded AI healing and improving scrape strategy validation and product insights.
Docstring Coverage ⚠️ Warning Docstring coverage is 25.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/api-store-currency-locale

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/Services/AiConfigHealer.php (1)

402-415: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Make the selector/regex validation tools stop when the extraction budget is exhausted.

HealingContext::validate() only checks whether HTML is loaded before running StandardStrategyDto and parsing the page offline. Since TestCssSelectorTool and TestRegexTool call validate() without checking $context->budget?->isExhausted(), the agent can continue looping past the remaining budget after the fetch tool has already stopped. Add the same budget exhaustion guard to validate() before executing a selector/regex test.

🤖 Prompt for 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.

In `@app/Services/AiConfigHealer.php` around lines 402 - 415, Update
HealingContext::validate() to check $context->budget?->isExhausted() before
loading StandardStrategyDto or parsing HTML, and stop consistently when the
extraction budget is exhausted. Preserve the existing HTML-loaded validation for
remaining selector and regex test calls, including TestCssSelectorTool and
TestRegexTool.
🧹 Nitpick comments (4)
tests/Feature/Api/MetaExtractionApiTest.php (1)

264-264: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Some sibling opt-out tests no longer prove their condition.

Adding heal => true here is correct. However, test_does_not_heal_when_store_opted_out and test_does_not_heal_when_healing_feature_is_disabled still post without the flag. Healing is now skipped for those requests because heal defaults to false, so the assertions pass regardless of the store opt-out and the feature flag. Add 'heal' => true to both requests so they exercise the paths they name. MetaExtractionBudgetTest covers the opt-out case, but these tests are now tautological.

🤖 Prompt for 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.

In `@tests/Feature/Api/MetaExtractionApiTest.php` at line 264, Update the requests
in test_does_not_heal_when_store_opted_out and
test_does_not_heal_when_healing_feature_is_disabled to include heal => true,
ensuring each test actually reaches and validates its respective opt-out
condition while preserving their existing assertions.
tests/Feature/Api/MetaExtractionBudgetTest.php (1)

241-245: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The elapsed-time bound is tight enough to flake.

self::BUDGET is 2 seconds, and this assertion covers the whole request, including bootstrapping, Sanctum authentication, the database round trip, and the faked scrape. On a loaded CI runner that can exceed 2 seconds even though no provider call was made. The other timing assertions use self::BUDGET + 3. Consider asserting against a value that is clearly below the blocking path but above normal request overhead, or assert that runAgent was never called instead of measuring time.

🤖 Prompt for 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.

In `@tests/Feature/Api/MetaExtractionBudgetTest.php` around lines 241 - 245, Relax
the elapsed-time assertion in this test to avoid measuring unrelated request
overhead, using the established self::BUDGET + 3 tolerance used by the other
timing assertions; alternatively, verify directly that runAgent was never called
while preserving the assertion that the known-failing provider is skipped.
app/Services/Ai/AiProviderHealth.php (1)

46-55: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The failure window slides; the comment states a fixed window.

Cache::put rewrites the counter with a full $cooldown TTL on every failure. Failures that arrive slightly faster than the cooldown therefore keep the counter alive and accumulate to the threshold, which the comment says cannot happen. Either accept the sliding window and correct the comment, or keep the first-failure expiry by writing the counter only when it is absent.

♻️ Option: preserve the original window
-        $failures = (int) Cache::get($this->failureKey($provider), 0) + 1;
-        Cache::put($this->failureKey($provider), $failures, $cooldown);
+        $key = $this->failureKey($provider);
+        // add() only writes when the key is absent, so the window starts at the first
+        // failure and is not extended by later ones.
+        Cache::add($key, 0, $cooldown);
+        $failures = (int) Cache::increment($key);
🤖 Prompt for 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.

In `@app/Services/Ai/AiProviderHealth.php` around lines 46 - 55, Update the
failure-counting logic in AiProviderHealth so the cooldown represents a fixed
window from the first failure: preserve the existing counter TTL when
incrementing and only establish it when the counter is absent, while keeping
threshold handling and open-breaker behavior unchanged.
app/Services/MetaExtractionService.php (1)

163-169: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Log the swallowed throwable.

$e is captured but never used, so an unexpected failure from previewForUrl leaves no trace. The healer logs AiProviderException itself, but any other throwable disappears here. Add a log line so this failure mode is diagnosable. Static analysis may also flag the unused variable.

♻️ Proposed change
         } catch (Throwable $e) {
+            Log::warning('Meta-extraction healing failed.', [
+                'url' => $url,
+                'exception' => $e::class,
+                'message' => $e->getMessage(),
+            ]);
+
             $outcome->failed($this->healFailureReason($budget));

Add use Illuminate\Support\Facades\Log; to the imports.

🤖 Prompt for 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.

In `@app/Services/MetaExtractionService.php` around lines 163 - 169, Update the
catch block around AiConfigHealer::previewForUrl in MetaExtractionService to log
the caught Throwable via Laravel’s Log facade before marking the outcome failed
and returning null; add the Log import and include useful exception details
while preserving the existing failure flow.
🤖 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 `@app/Services/ExtractionBudget.php`:
- Around line 44-47: Update app/Services/ExtractionBudget.php in
remainingSecondsForTimeout() to return an explicit no-safe-start state when less
than one second remains instead of rounding up to one. In
app/Services/Ai/HealingContext.php, make fetch() reject that state and cap any
explicit timeout to the remaining budget. Update
tests/Unit/Services/ExtractionBudgetTest.php to assert rejection of the
no-safe-start state rather than expecting a one-second timeout.

---

Outside diff comments:
In `@app/Services/AiConfigHealer.php`:
- Around line 402-415: Update HealingContext::validate() to check
$context->budget?->isExhausted() before loading StandardStrategyDto or parsing
HTML, and stop consistently when the extraction budget is exhausted. Preserve
the existing HTML-loaded validation for remaining selector and regex test calls,
including TestCssSelectorTool and TestRegexTool.

---

Nitpick comments:
In `@app/Services/Ai/AiProviderHealth.php`:
- Around line 46-55: Update the failure-counting logic in AiProviderHealth so
the cooldown represents a fixed window from the first failure: preserve the
existing counter TTL when incrementing and only establish it when the counter is
absent, while keeping threshold handling and open-breaker behavior unchanged.

In `@app/Services/MetaExtractionService.php`:
- Around line 163-169: Update the catch block around
AiConfigHealer::previewForUrl in MetaExtractionService to log the caught
Throwable via Laravel’s Log facade before marking the outcome failed and
returning null; add the Log import and include useful exception details while
preserving the existing failure flow.

In `@tests/Feature/Api/MetaExtractionApiTest.php`:
- Line 264: Update the requests in test_does_not_heal_when_store_opted_out and
test_does_not_heal_when_healing_feature_is_disabled to include heal => true,
ensuring each test actually reaches and validates its respective opt-out
condition while preserving their existing assertions.

In `@tests/Feature/Api/MetaExtractionBudgetTest.php`:
- Around line 241-245: Relax the elapsed-time assertion in this test to avoid
measuring unrelated request overhead, using the established self::BUDGET + 3
tolerance used by the other timing assertions; alternatively, verify directly
that runAgent was never called while preserving the assertion that the
known-failing provider is skipped.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c456b027-5f7d-407b-897c-64bcb7a2ced4

📥 Commits

Reviewing files that changed from the base of the PR and between f905cec and c0c8d0a.

📒 Files selected for processing (24)
  • app/Dto/HealingOutcomeDto.php
  • app/Enums/HealingReason.php
  • app/Filament/Resources/StoreResource/Api/Requests/CreateStoreRequest.php
  • app/Filament/Resources/StoreResource/Api/Requests/UpdateStoreRequest.php
  • app/Http/Controllers/Api/ClientConfigController.php
  • app/Http/Controllers/Api/MetaExtractionController.php
  • app/Http/Requests/MetaExtractionRequest.php
  • app/Http/Resources/MetaExtractionResource.php
  • app/Rules/ScrapeStrategyValue.php
  • app/Services/Ai/AiProviderHealth.php
  • app/Services/Ai/HealingContext.php
  • app/Services/AiConfigHealer.php
  • app/Services/AiService.php
  • app/Services/ExtractionBudget.php
  • app/Services/MetaExtractionService.php
  • config/price_buddy.php
  • docs/docs/api.md
  • tests/Feature/Api/ClientConfigApiTest.php
  • tests/Feature/Api/MetaExtractionApiTest.php
  • tests/Feature/Api/MetaExtractionBudgetTest.php
  • tests/Feature/Api/StoreScrapeStrategyValidationTest.php
  • tests/Feature/Services/AiServiceHealthTest.php
  • tests/Unit/Services/AiProviderHealthTest.php
  • tests/Unit/Services/ExtractionBudgetTest.php

Comment thread app/Services/ExtractionBudget.php Outdated
jez500 added 3 commits August 8, 2026 19:11
A product said "Great time to buy" the moment it was added. With a flat or
single-point history the beat fraction is 0, but the current price is trivially
the lowest ever seen, so isAllTimeLow fired and floored the score at 9.5. The
claim was about a price nothing had been compared to.

DealScoreCalculator now takes hasPriceVariation and beatsOtherListings, and
short-circuits before the percentile and all-time-low logic when the price has
never moved:

- no variation, not the cheapest listing -> new `unknown` verdict, "Not enough
  data yet", score 0
- no variation but cheaper than the product's other URLs -> "Great time to buy",
  score 8. Not history, but real evidence: it is the best place to buy this today
- variation -> unchanged

isAllTimeLow is false in both no-variation cases. A low only means something next
to a high, and the flag also drives the hero's "cheapest it has ever been" line
and the dashboard's ordering.

Side effect worth having: the dashboard's "Buy now" section filters on
dealScore.score >= 6, so every newly added product used to land in it at 9.5.
An `unknown` product scores 0 and stays out.

Recomputed against real data: 3 of 11 products change, all of them two readings
at an identical price on a single URL. The rest are untouched. Existing products
keep their cached verdict until the next price change or
`artisan buddy:regenerate-price-cache`.
The "should I buy right now?" card had a hardcoded teal gradient, so a red
"Wait — it's expensive right now" sat on a card that read as good news.

The verdict now picks a Filament palette (great/good -> primary, average/unknown
-> gray, pricey -> warning, wait -> danger) which drives the gradient, the ring,
the heading, the verdict text and the percentile pill together.

Colours go through Filament's runtime CSS variables rather than text-*/bg-*
utilities, since the palettes are injected by the panel and are not in the
compiled CSS. The shades are also set as local custom properties on the card so
the text keeps a dark-mode variant, which an inline style cannot carry on its own.

The inline border-color this replaces was dead: the card uses ring-1, not a
border, so it never rendered. It is now a --tw-ring-color override, which also
tints the outline in dark mode.

Verified by screenshotting three real products: wait renders red, pricey amber,
great teal as before.
remainingSecondsForTimeout() rounded up to one second whenever less than one
remained. Since isExhausted() only covers <= 0, a fetch with 0.4s left was both
allowed and given a full second, overspending the deadline on every call - up to
25 times through the agent's tool loop.

It now returns null below one second: an explicit "cannot start", never confused
with Guzzle's "0 means no timeout". HealingContext::fetch() rejects that state -
the null check subsumes its old isExhausted() guard - and caps any explicit
timeout at the remaining budget, so the parameter cannot become a way past the
deadline.

Two other call sites needed the same distinction:

- AiConfigHealer passes this value straight to runAgent(timeout:), where null
  means "use the provider's timeout". Passing the nullable value through would
  have silently restored the unbounded 120s call this whole mechanism exists to
  prevent, so it now separates "no budget" (unbudgeted callers keep the provider
  timeout) from "budget with nothing left" (stop).
- MetaExtractionService::scrapeTimeout() does min($this->timeout, ...), and
  min(10, null) is null in PHP, which would have hit setConnectTimeout(int). The
  one-second floor now lives there, where it belongs: the scrape runs first, so
  the budget is always full, and the floor only bites on a near-zero budget.

The new test pins the actual bug: with 2 seconds of budget and 1.2 elapsed the
budget is not exhausted and has real time left, but has no whole second to hand
out. The old code returned 1 there.

Live re-check against the wedged provider is unchanged: HTTP 200 in 24.16s with
reason "timeout".
@jez500
jez500 merged commit 15b1cf5 into main Aug 8, 2026
1 of 2 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 `@app/Services/ExtractionBudget.php`:
- Around line 48-50: Update app/Services/ExtractionBudget.php lines 48-50 in
remainingSecondsForTimeout() to sample remainingSeconds() once, returning null
when the sampled value is below one second and otherwise returning its floored
integer value. Update tests/Unit/Services/ExtractionBudgetTest.php lines 52-57
to assert that any non-null timeout is at least 1.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fd0b2149-384e-4ae0-b21c-798353a786ca

📥 Commits

Reviewing files that changed from the base of the PR and between a33963d and 69e6f58.

📒 Files selected for processing (5)
  • app/Services/Ai/HealingContext.php
  • app/Services/AiConfigHealer.php
  • app/Services/ExtractionBudget.php
  • app/Services/MetaExtractionService.php
  • tests/Unit/Services/ExtractionBudgetTest.php
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/Services/AiConfigHealer.php
  • app/Services/Ai/HealingContext.php
  • app/Services/MetaExtractionService.php

Comment on lines +48 to +50
public function remainingSecondsForTimeout(): ?int
{
return $this->hasAtLeast(1) ? (int) floor($this->remainingSeconds()) : null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Prevent a zero timeout at the one-second boundary.

remainingSecondsForTimeout() samples the clock twice. If the first sample reports at least one second and the second sample crosses below one second, the method returns 0 instead of null. The current test also accepts 0.

  • app/Services/ExtractionBudget.php#L48-L50: store remainingSeconds() once, then return null when that value is below one second.
  • tests/Unit/Services/ExtractionBudgetTest.php#L52-L57: assert that a non-null timeout is at least 1.
🛠️ Proposed fix
 public function remainingSecondsForTimeout(): ?int
 {
-    return $this->hasAtLeast(1) ? (int) floor($this->remainingSeconds()) : null;
+    $remaining = $this->remainingSeconds();
+
+    return $remaining >= 1 ? (int) floor($remaining) : null;
 }
         $this->assertNotNull($timeout);
+        $this->assertGreaterThanOrEqual(1, $timeout);
         $this->assertLessThanOrEqual(10, $timeout);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function remainingSecondsForTimeout(): ?int
{
return $this->hasAtLeast(1) ? (int) floor($this->remainingSeconds()) : null;
public function remainingSecondsForTimeout(): ?int
{
$remaining = $this->remainingSeconds();
return $remaining >= 1 ? (int) floor($remaining) : null;
Suggested change
public function remainingSecondsForTimeout(): ?int
{
return $this->hasAtLeast(1) ? (int) floor($this->remainingSeconds()) : null;
public function test_the_timeout_value_is_rounded_down_so_the_budget_is_not_overspent(): void
{
$timeout = (new ExtractionBudget(10))->remainingSecondsForTimeout();
$this->assertNotNull($timeout);
$this->assertGreaterThanOrEqual(1, $timeout);
$this->assertLessThanOrEqual(10, $timeout);
📍 Affects 2 files
  • app/Services/ExtractionBudget.php#L48-L50 (this comment)
  • tests/Unit/Services/ExtractionBudgetTest.php#L52-L57
🤖 Prompt for 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.

In `@app/Services/ExtractionBudget.php` around lines 48 - 50, Update
app/Services/ExtractionBudget.php lines 48-50 in remainingSecondsForTimeout() to
sample remainingSeconds() once, returning null when the sampled value is below
one second and otherwise returning its floored integer value. Update
tests/Unit/Services/ExtractionBudgetTest.php lines 52-57 to assert that any
non-null timeout is at least 1.

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