Munchable scans a barcode and tells you whether a product suits the gut conditions on your profile. People kept asking for something next to that: not "is this safe for my IBS", but "I do not want to buy things with artificial colours in them, tell me when there are some".
The cheap way to ship that is to add it as another condition. We already have a rules engine that takes a profile of conditions and returns a verdict per condition, so an eighth entry in the enum called "healthy" is maybe an afternoon of work.
It is also wrong, and the reason is worth more than the feature.
A preference is not a health claim, and the engine knows the difference
A condition rule set answers a clinical question using clinical guidance. The engine reconciles the per-condition results, detects conflicts between them, and takes the worst tier as the overall verdict, because when two conditions disagree about a product the cautious one has to win.
Drop a preference into that machinery and it inherits all of it. "Contains an artificial colour" would argue with a low FODMAP assessment as though the two were the same kind of statement, and the worst-tier rule would let a colouring agent out-vote a clinical result. The answer on screen would be a single word covering two questions that have nothing to do with each other.
So the healthy filter is not a condition. It is a third lane beside the conditions and the allergen layer, and it has three properties none of the others have:
- It runs only for a profile that switched it on, and is absent from the result entirely for everybody else.
- It never produces a verdict word. The conditions own "Good fit", "Caution" and "Avoid", and this layer is not allowed to use them.
- It never clears a product. It can only say what it found.
That last one is the whole design, and it lives in about ten lines of presentation code.
Never green
/** The row title: the finding count, or the quiet state. */
export function healthTitle(check: HealthCheck): string {
const n = check.hits.length;
if (n === 0) return 'Nothing you asked about';
return n === 1 ? '1 thing you asked about' : `${n} things you asked about`;
}
/** The word on the right of the row. Never a verdict word. */
export function healthLabel(check: HealthCheck): string {
return check.hits.length === 0 ? 'None found' : 'Found';
}
"Nothing you asked about" rather than "nothing to worry about". The filter reads an ingredient list and whatever nutrition figures the row happens to carry. "Nothing you asked about" is a true sentence. "Nothing to worry about" is a claim about the whole pack that no ingredient list can support.
The colour follows the same rule: amber when something is found, muted grey when nothing is, and deliberately never green. A green tick beside "none found" reads as approval of the product, and this layer has no opinion about the product. The icon changes too, so the state is never carried by colour alone.
The tone is different from the allergen rows on purpose. Allergens are safety critical, so they hedge: "not mentioned" must never be read as "free from". The healthy filter does not hedge at all, because the reader set this preference themselves. They asked not to see artificial colours, the pack has one, and naming it is the entire job. Stacking a caveat under a line the reader requested is noise pretending to be diligence.
The tier is what makes it useful
The obvious implementation of "flag additives" flags everything. Citric acid and lecithin are on tens of thousands of products in our catalogue. A filter that fires on "this is an additive" fires on most of the aisle, and a warning on nearly every product is not a warning, it is wallpaper.
So each additive carries a concern tier, and only one of the three ever fires:
| tier | behaviour |
|---|---|
flagged |
named, when the preference covering its class is on |
notable |
never fires, listed as context under the row |
benign |
silent |
There is a second, quieter benefit. A classification is an assessment. Before this existed, every one of those additives showed up on the result screen's "not yet assessed" list and in the curation backlog, for every user, whether or not they had the filter on. "This is citric acid and there is nothing to say about it" is an answer, not a gap.
You can see that layer working on the public question pages, which are generated by the same engine: Does E330 cause reflux? and Does E270 cause reflux? are both additives that a naive filter would shout about, and neither page invents a concern. The full index is here.
What automation is allowed to touch
The additive map grows two ways: by hand, and through the same curation pipeline that extends the rest of our taxonomy. The boundary is asymmetric on purpose.
An automated row may classify an additive, and may file it as notable or benign. It may never mark one flagged.
Deciding what the filter is for is a product judgement, and a wrong one puts a caution on somebody's shopping. Widening the silent set is a cleanup. Widening the loud set is a claim. So the loud set grows by hand, with a person on it, which is the same rule the rest of the engine follows and which we wrote about in AI proposes, the engine disposes.
Two small structural choices that saved trouble
One array, not a boolean and a list. The on-device profile stores a single array of enabled preferences, and an empty array means the filter is off. A boolean beside a list is two sources of truth for one question, and they will disagree: "on, with nothing selected" is a filter that checks nothing while the settings screen cheerfully says it is working.
A Record over the id type, not a lookup that can miss. Every preference the engine knows about must have a card in the app:
/**
* `Record` over `HealthPreferenceId` is what guarantees every engine preference
* has a card: leave one out and this file does not compile, which is a better
* place to find out than a crash on the settings screen.
*/
const META: Record<HealthPreferenceId, Omit<HealthPreferenceMeta, 'id'>> = { ... };
Adding a preference to the engine and forgetting the UI is now a build failure rather than a blank row somebody finds in production.
And the thresholds are somebody else's. The nutrient preferences compare against the UK FSA front-of-pack red bands, per 100 g, unchanged. A published, citable standard on purpose: the number a shopper has already seen on the front of thousands of packs is the number this filter should agree with. Inventing our own cut-off would mean disagreeing with the label in their hand, and being right would not help.
The general shape
If you are bolting a preference system onto something that already produces a judgement, the questions worth asking early are: can this new layer change the existing verdict (ours cannot), can it clear something (ours cannot), and does it use the same vocabulary as the thing that can (ours may not). Three noes, and the two systems can sit on the same screen without either one borrowing the other's authority.
The public conditions pages show the side of this that does give verdicts, for contrast. The difference in tone between those pages and a preference row is the whole point.
Top comments (0)