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:
-
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.
-
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.
-
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.
-
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:
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:
Definition of done
Plan produced via /grill-me design interview; see /idea-to-issue workflow.
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-mesession, walking through the design tree one fork at a time: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 bothMipStrategyand theGreedyCaloriesheuristic (src/engine/orchestrator.py:34-49routes requests with >50 products to the heuristic) consume the same product list from. A MIP-only constraint component (mirroringConstraintLimitWeight/ConstraintLimitBudget) would silently fail to prohibit low-calorie products for large requests.Threshold storage — A hardcoded module-level constant in
preprocessing.py(e.g.MIN_CALORIES_ALLOWED = 20), not aSettingsfield and not a per-request JSON parameter.Settings(src/services/settings.py) only holds I/O/solver config today and isn't threaded intoPreProcess.run()— adding it there for one constant is unjustified plumbing for a fixed business rule that wasn't asked to be user-configurable.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.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
.lpoutput.Files to touch
src/engine/preprocessing/preprocessing.py— addMIN_CALORIES_ALLOWED = 20constant; extend thefeasible_productscomprehension withand p.calories > MIN_CALORIES_ALLOWED; update the module docstring (currently frames filtering as only physical infeasibility — lines 6-10) and therun()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 thefeasible_productsattribute 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 insolve()(lines 79-80: "Every product costs more than the budget or weighs more than the capacity") — both currently assume an emptyfeasible_productslist 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 existingtest__preprocess__run__<behavior>naming and Arrange/Act/Assert style already used in this file.tests/resources/low_calorie_product_filtered/data.jsonandmodel.lp(new) — integration fixture mirroringtests/resources/individually_infeasible_product_filtered/, picked up bytests/integration/test_situations.pyvia thelp_comparerhelper.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 withcalories=20must 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 withcalories=21must 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.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 usebanana(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 asindividually_infeasible_product_filtered/data.json, with one product atcalories <= 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'smodel.lpwas (solve once, capture output, verify by inspection).Definition of done
pytest -m "not integration"passes (fast unit suite, including the newtest_preprocess.pycases)pytest(full suite /integrationmarker) passes, including the newtest_situations.pyfixtureblack src testspasses cleanmypypasses cleanPlan produced via
/grill-medesign interview; see/idea-to-issueworkflow.