DEV Community

Daniel Pertu
Daniel Pertu

Posted on

Four files in our monorepo print the same legally required sentence, and three of them disagree about its wording

Munchable answers questions about packaged food for people with gut conditions, which means a certain amount of its copy is not copy. It is a requirement. "Not a medical device", "always read the physical label", "a warning is not an all clear": those sentences have to be on the screen, they have to say a specific thing, and a developer must not be able to improve them in passing.

The phone app treats that as a module. The website treats it as prose. I went looking for how far apart they had drifted, and the answer is further than I expected.

The phone app's version, which I think is right

src/strings.ts, 88 lines, every required sentence an exported constant with a comment saying which section of the spec demands it. The screens import them. No required sentence is ever typed inside a component.

The first line of the file is the interesting one:

import { DISCLAIMER } from '@munchable/rules-engine';

export const SCAN_DISCLAIMER = DISCLAIMER;
Enter fullscreen mode Exit fullscreen mode

The mandatory notice is not owned by the app at all. It is owned by the package that produces the answer, and that package also puts it on every result it returns:

return {
  verdict,
  confidence,
  // ...
  disclaimer: DISCLAIMER,
};
Enter fullscreen mode Exit fullscreen mode

I like that more than it probably deserves. The sentence travels with the thing it qualifies, as a field, so a surface that renders an answer has the caveat in its hand already. Forgetting it requires actively dropping a property rather than merely failing to remember a convention.

Region is a parameter, not a stack of paragraphs

"Medical device" is a defined term with different definitions in different places, so the secondary notices are a function of region rather than a block that lists every jurisdiction at once:

export function extraDisclaimers(region: RegionId): { key: string; text: string }[] {
  const medical = region === 'eu' || region === 'uk' ? NOT_MEDICAL_EU : NOT_MEDICAL_ADVICE;
  return [
    { key: 'medical', text: medical },
    { key: 'allergy', text: ALLERGY_FIREWALL },
  ];
}
Enter fullscreen mode Exit fullscreen mode

Four regions (US, UK, EU, somewhere else). The mandatory notice is always visible; these sit behind a "More" expander, because a result that opens as a wall of legal text is a result nobody reads, including the parts that matter.

One notice deliberately has no caveat attached

This is the part of the file I would defend hardest, and it is a comment rather than code:

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.

The allergy notice carries a long sentence saying that a warning only ever adds caution and never means a product is free from anything. The other optional filter in the app carries nothing of the kind, on purpose.

A caveat attached to everything is a caveat attached to nothing. You get the pharmaceutical advert effect, where the disclaimer is a texture the eye skips, and the one place it was load bearing goes down with the rest. Deciding which notices do not get a caveat is the same job as writing the ones that do.

The audit

The required sentence, in the package that owns it:

// packages/rules-engine/src/constants.ts
export const DISCLAIMER =
  'Based on the product’s listed ingredients. Always read the physical label.';
Enter fullscreen mode Exit fullscreen mode

The same sentence, in the website's API response copy:

// apps/web/lib/lookup.ts
export const DISCLAIMERS = {
  readLabel: 'Based on the listed ingredients. Always read the physical label.',
  // ...
};
Enter fullscreen mode Exit fullscreen mode

And in the website's footer, as JSX:

<p>
  Verdicts are based on a product's listed ingredients. Always read the
  physical label.
</p>
Enter fullscreen mode Exit fullscreen mode

Three literals. "the product's listed ingredients", "the listed ingredients", "a product's listed ingredients", one of them with a typographic apostrophe and the others without. The allergy sentence has two wordings too, one saying "the allergies you list" and the other "the allergens you list".

Nothing here is wrong, which is exactly why it survived. Every version says the required thing. But three literals is three places to update when counsel asks for a word change, and the failure mode is not a crash, it is a page that quietly keeps the old sentence for a year.

You can read the public half of this on any page of the site, because the footer block appears on all 426 of them: the about page states it at length, and the same three paragraphs sit under the SIBO page and under every one of the generated question pages like is apple low FODMAP. The terms are the formal version.

Why our existing copy test would never have caught it

There is a unit test in this repo that enforces house style on visible words. It walks every source file in the directories that can produce copy and fails on a character we have banned from user-facing text. It has caught real things.

It is also scoped to one workspace's visible directories, so a divergence between a package and two apps is not the kind of thing it looks for. A character ban generalises for free; a sentence identity check does not.

The check that would have caught this is the inverse shape: given the set of required notices exported by the package that owns them, assert that no other workspace contains a second literal of any of them. Cheap to write (read the constants, grep the tree, exempt the owning file), and it fails the moment somebody pastes rather than imports.

What the fix looks like

Not "be more careful". The structure is wrong in one specific place: the footer is the only surface where a required notice is written as content rather than imported as a dependency, and the API response copy is a convenience copy that should be a re-export.

  • The required notices move into the package both apps already depend on.
  • The footer imports them. Regulatory copy stops being a thing a designer can edit in a component.
  • The API response copy re-exports rather than restates.
  • The test above runs on every build, so the next person who pastes gets a red build instead of a slow divergence.

The phone app got this right on the first pass because the spec it was written from listed the sentences as requirements with section numbers. The website got it wrong because a footer looks like a place for words. That is the whole lesson: in a product where some copy is a requirement, the requirement needs to arrive at the component the same way any other dependency does.

Top comments (0)