Skip to content

Prohibit products with 20 calories or less from the optimization model #4

Description

@fjzs

Problem

User feedback: the optimizer can currently select products with very low nutritional value. We need a hard business rule: any product with 20 calories or less must never be selectable, regardless of its price/weight attractiveness.

Design decisions

Resolved via a /grill-me session, walking through the design tree one fork at a time:

  1. Where the rule lives — Extend the existing feasibility filter in PreProcess.run() (src/engine/preprocessing/preprocessing.py:40-44), not a MIP-only constraint component. This is the only place in the pipeline that both MipStrategy and the GreedyCalories heuristic (src/engine/orchestrator.py:34-49 routes requests with >50 products to the heuristic) consume the same product list from. A MIP-only constraint component (mirroring ConstraintLimitWeight/ConstraintLimitBudget) would silently fail to prohibit low-calorie products for large requests.

  2. Threshold storage — A hardcoded module-level constant in preprocessing.py (e.g. MIN_CALORIES_ALLOWED = 20), not a Settings field and not a per-request JSON parameter. Settings (src/services/settings.py) only holds I/O/solver config today and isn't threaded into PreProcess.run() — adding it there for one constant is unjustified plumbing for a fixed business rule that wasn't asked to be user-configurable.

  3. Implementation shape — Extend the existing single list comprehension with one more condition (p.calories > MIN_CALORIES_ALLOWED), matching how weight and budget are already combined in one filter. Do not split into separate predicate methods or a new preprocessing class — that's more structure than a one-line condition warrants in this codebase.

  4. Test coverage — Both a unit test suite (fast, isolated) and a new integration fixture (mirroring the existing physical-infeasibility fixture), so the exclusion is verified both in-memory and in the solved .lp output.

Files to touch

  • src/engine/preprocessing/preprocessing.py — add MIN_CALORIES_ALLOWED = 20 constant; extend the feasible_products comprehension with and p.calories > MIN_CALORIES_ALLOWED; update the module docstring (currently frames filtering as only physical infeasibility — lines 6-10) and the run() method docstring (lines 27-38) to describe two distinct exclusion reasons: physically infeasible (price/weight) and policy-prohibited (calories).
  • src/engine/preprocessing/pre_processed_data.py — update the feasible_products attribute docstring (lines 19-22), which currently only mentions weight/budget fit.
  • src/engine/orchestrator.py — update _select_strategy's docstring (lines 35-39, says "infeasible products were already removed") and the in-line comment in solve() (lines 79-80: "Every product costs more than the budget or weighs more than the capacity") — both currently assume an empty feasible_products list can only mean physical infeasibility, which is no longer true once calorie exclusion is added.
  • tests/engine/optimization_strategy/mip/preprocess/test_preprocess.py — add new test cases (see TDD plan below), following the existing test__preprocess__run__<behavior> naming and Arrange/Act/Assert style already used in this file.
  • tests/resources/low_calorie_product_filtered/data.json and model.lp (new) — integration fixture mirroring tests/resources/individually_infeasible_product_filtered/, picked up by tests/integration/test_situations.py via the lp_comparer helper.

TDD plan (red → green → refactor) — acceptance criteria

Write these failing tests first, in test_preprocess.py:

  • test__preprocess__run__excludes_product_with_exactly_threshold_calories — a product with calories=20 must be excluded (the rule is "20 or less", i.e. an inclusive boundary).
  • test__preprocess__run__keeps_product_with_calories_just_above_threshold — a product with calories=21 must be kept.
  • test__preprocess__run__excludes_low_calorie_product_from_mixed_catalogue — a catalogue mixing a low-calorie product with valid ones keeps only the valid ones.
  • Confirm existing tests in the same file (keeps_product_that_fits_within_both_constraints, excludes_product_whose_price_exceeds_budget, excludes_product_whose_weight_exceeds_capacity, keeps_only_feasible_products_from_mixed_catalogue) stay green — they use banana (89 cal) and other fixtures already well above the new threshold, so no fixture changes should be needed there.

Then implement the constant + filter change to go green, then refactor docstrings/comments per .claude/skills/comment-code/SKILL.md (module and method docstrings, and a short why-comment on the boundary condition explaining that "20 or less" is prohibited, i.e. the code keeps strictly > 20).

Then add the integration fixture:

  • data.json: same shape as individually_infeasible_product_filtered/data.json, with one product at calories <= 20 (e.g. 15) alongside otherwise-valid products.
  • model.lp: expected solver output showing the low-calorie product's variable absent, generated the same way the existing fixture's model.lp was (solve once, capture output, verify by inspection).

Definition of done

  • pytest -m "not integration" passes (fast unit suite, including the new test_preprocess.py cases)
  • pytest (full suite / integration marker) passes, including the new test_situations.py fixture
  • black src tests passes clean
  • mypy passes clean

Plan produced via /grill-me design interview; see /idea-to-issue workflow.

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions