Munchable reads a food label and tells you whether a product suits a digestive condition you have. Under every answer it gives sits one sentence:
Based on the product's listed ingredients. Always read the physical label.
Eleven words. It is not legal boilerplate, it is the honest scope of the answer above it: the product knows what a label says, and a label is not the same thing as a food. Everything downstream of that, what the app is allowed to claim, what it must never claim, and where each caveat is written down, turns out to be an architecture problem rather than a copywriting one.
This post is the map of those sentences, the two that drifted, and the one surface that deliberately has none.
The caveat lives with the thing it caveats
The canonical version is a constant in the rules engine package, next to the condition labels:
/** Rendered on every scan result (PLAN.md §6, §7). */
export const DISCLAIMER =
'Based on the product’s listed ingredients. Always read the physical label.';
And the object the engine returns for every check carries it as a field, beside the verdict and the reasons:
disclaimer: DISCLAIMER,
That is the decision worth copying. The caveat is a field on the result, not a string a screen remembers to render. Anything holding a result is holding the sentence that bounds it, which means a new surface cannot accidentally ship the answer without the scope. The consumers prove the point: the app's string module imports it rather than restating it,
/**
* Required legal/regulatory strings (PLAN.md §4, §13; SCREENS.md §0).
* `SCAN_DISCLAIMER` reuses the engine's exact `DISCLAIMER` verbatim.
*/
import { DISCLAIMER } from '@munchable/rules-engine';
export const SCAN_DISCLAIMER = DISCLAIMER;
and so does the package that renders promotional video, where the same sentence sits under a mocked-up result on a ten second clip:
import { CONDITION_LABELS, DISCLAIMER } from '@munchable/rules-engine';
A marketing asset showing a verdict with no caveat is exactly the sort of thing that happens when a designer recreates a screen in a video tool. It cannot happen here, because the video imports the same constant the product does.
Then I counted the copies
Three places render a version of that sentence to a user, and all three say something slightly different.
The engine, as above:
Based on the product’s listed ingredients. Always read the physical label.
The API response copy, in the web app's product lookup module:
export const DISCLAIMERS = {
readLabel: 'Based on the listed ingredients. Always read the physical label.',
// ...
};
And the website footer, typed into JSX:
<p>
Verdicts are based on a product's listed ingredients. Always read the
physical label.
</p>
"the product's listed ingredients", "the listed ingredients", "a product's listed ingredients". The first uses a typographic apostrophe, the third a straight one. Nothing is wrong with any of them in isolation, and that is how you get three: each was written where it was needed, by someone who had the sentence in their head rather than in their editor, and each reads fine on its own page.
There is a fourth, in a doc comment on the result type:
/** "Based on listed ingredients — always read the physical label." */
disclaimer: string;
That one is nobody's copy. It documents the field by quoting a wording the constant has not used for some time, with an em dash in it, which is banned from this product's copy and perfectly legal here because comments never render. A doc comment that paraphrases a constant is a copy of that constant with no test and no reader, which is the ideal conditions for going stale.
The one test, and what it chose to assert
The engine has exactly one test about any of this:
test('every result carries the read-the-label disclaimer', () => {
const r = fitCheck(product({ ingredientsTags: ['en:water'] }), { conditions: ['ibs'] });
assert.match(r.disclaimer, /read the physical label/i);
});
Five words, case insensitive. Every one of the four wordings above passes it.
I went back and forth on whether that is the bug or the design, and I have landed on design, with a missing test beside it. What the engine owes its callers is that no result escapes without telling the reader to check the pack. That is what the assertion says, and an assertion on the exact string would fail on a comma change while telling you nothing more about the promise. The engine should test the promise.
What is missing is a different test in a different place: one that asserts the three user-visible copies are one string. That test cannot live in the engine, because the engine does not know the website has a footer. It belongs where the footer is, and the correct fix is for the footer to stop having its own sentence at all.
The difference that is deliberate
Not every variation is drift. The recipe library has its own caveat, and the reason is written above it:
* The engine's own `DISCLAIMER` tells the reader to check the physical label,
* which is the right instruction for a packaged product and meaningless for a
* pan of food. A recipe needs the equivalent honest caveat for its own shape.
* It matters more here than it would on a screen full of verdicts: the reader
* is shown this recipe *because* it suits them, and that claim is about the
* recipe as written, not about whatever they improvise from it.
export const RECIPE_DISCLAIMER =
'Checked against this recipe as written. Swap an ingredient or change the portion and the check changes with it.';
"Always read the physical label" under a recipe would be a sentence doing nothing, and a caveat that does nothing teaches a reader to skip the next one.
A recipe gets a second note, on ingredient lines that are themselves packaged goods:
* Munchable knows the generic ingredient. It cannot know what is in the
* particular jar in someone's cupboard, and a bought stock, bread or sauce
* routinely carries onion, garlic or wheat that this recipe does not name.
* Saying so is what keeps a green recipe verdict truthful.
export const PACKAGED_NOTE =
'Brands differ. Scan the one in your cupboard to check what it actually contains.';
Which is my favourite of all of them, because it turns a caveat into the product's other feature. The honest limit of the recipe check is exactly the thing the barcode scanner is for.
The absence that is deliberate
The sharper decision is where a caveat is left out on purpose. The app has an allergy notice, and it carries a firewall sentence:
export const ALLERGY_FIREWALL =
'Munchable warns about the allergies you list when the label data names them. It cannot see every label, every recipe change or every factory, so never rely on it alone. Always read the pack. Lactose intolerance is not a milk allergy.';
It also has a shopping filter, where a reader picks additives and nutrients they would rather not buy. That one has a heading and no caveat at all, and the comment in the strings module explains the asymmetry:
* Healthy shopping (docs/ARCHITECTURE.md "Healthy shopping"). Note what is NOT
* here: there is no firewall sentence to sit beside this one. The allergy
* notice needs its caveat because a missed warning is a safety failure and
* "not mentioned" must never read as "free from"; this filter answers a
* question the reader set themselves, so it names what it found and stops.
That is the test I now apply to every caveat in the product. Not "could this be misread?", which is always yes, but "what is the cost of the specific misreading?" Silence about an allergen can hurt somebody. Silence about an additive somebody asked to avoid is just silence. Attaching a safety paragraph to the second one would dilute the first.
Secondary caveats are regional, and behind an expander
Below the mandatory line there is a second tier, and it varies by where the reader is:
/**
* Region-scoped secondary disclaimers (PLAN §4, §11). The mandatory
* `SCAN_DISCLAIMER` is always shown separately; these live behind a "More"
* expander so a Result isn't a wall of legal text.
*/
export function extraDisclaimers(region: RegionId) {
const medical = region === 'eu' || region === 'uk' ? NOT_MEDICAL_EU : NOT_MEDICAL_ADVICE;
return [
{ key: 'medical', text: medical },
{ key: 'allergy', text: ALLERGY_FIREWALL },
];
}
The EU and UK wording names the regulatory category ("not a medical device under EU/UK rules") because there the category is a defined thing with consequences; elsewhere the sentence says the same thing in plainer words.
The structure is the part I would defend hardest. One sentence is always visible and never collapsible. Everything else is one tap away. A result screen that opens as four paragraphs of hedging gets scrolled past as a block, which is a worse outcome for the paragraph that mattered than putting the other three behind "More".
The allergy firewall has two copies too
The API response carries its own:
allergyFirewall:
'Munchable warns about the allergens you list when the label data names them. It cannot see every label, every recipe change or every factory, so never rely on it alone. Always read the pack.',
Compared with the app's version: "allergens" against "allergies", and the app's ends with one extra sentence, "Lactose intolerance is not a milk allergy."
That extra sentence is there because the confusion is extremely common and the two conditions sit next to each other in the app's own picker, where somebody with lactose intolerance can easily tick a milk allergy and get a stricter product than they wanted, or the reverse. The API has no picker, so nobody writing that string was thinking about it.
Which is the whole pattern in one example: the copies do not drift randomly. Each one drifts toward the context it was written in, and the context it was written in is exactly what the other copy lacks.
What is still wrong with this
- Three live wordings of the mandatory sentence. The footer and the API should both import the engine's constant. The footer is the worse of the two, because it is the one a crawler reads on all 516 URLs in the sitemap.
- The doc comment is stale. A paraphrase of a constant, drifted, in a file that defines the shape of every result.
- The only assertion is on five words. Correct for the engine, and no test anywhere asserts that the sentences a user reads are the same sentence.
- Two allergy firewalls, one of which has an extra safety sentence. The one with the extra sentence is the one on the surface where the confusion happens, which is luck rather than design: nothing would have caught it if the sentence had landed in the other copy.
- The deliberate absence is a comment. Somebody adding a caveat to the shopping filter out of caution would be doing an obviously reasonable thing, and the only thing standing in their way is a paragraph of prose explaining why not.
Read them on the live pages
The recipe caveat and the packaged ingredient note are both public: open any recipe and read the "How this recipe was checked" section at the bottom, then look at the note under the ingredient list where one of the lines is itself a packet. The footer wording, the one that drifted, is at the bottom of every page on the site.
For the allergy firewall and the never-says-free-from rule, the about page has a section called "What Munchable is not", the coeliac disease guide applies the same rule to gluten specifically, and the hidden gluten reference page answers it as a question. Those three say the same thing three times in three registers, on purpose, which is the opposite of the drift described above and is the reason the drift is worth fixing: when the repetition is deliberate it should be exact.
Related: the one careful sentence on our reference page is not written in the page, it is read off the product at build time, which is the same idea applied to a claim that had to be exactly true.
Top comments (0)