New York caps 1,4-dioxane in a finished cosmetic at 10 parts per million.1 1,4-Dioxane isn't an ingredient. It forms as a byproduct when certain detergents, foaming agents, emulsifiers and solvents are manufactured. The FDA lists the syllables that identify those ingredients by name: PEG, polyethylene, polyethylene glycol, polyoxyethylene, -eth- and -oxynol-. The ingredients themselves are legal in New York. In the ingredient-compliance product I built, curation is held to a rule I derived from that statute: an entry may be prohibited or restricted only when a label can identify the restricted thing. So the New York entry that matches Laureth-4 is a watchlist entry. A label names Laureth-4 but can't show whether it carries any 1,4-dioxane. When the rule was set, the product's 19 curated standards held 1,951 entries, and the rule moved four of them, all in the New York file, from restricted to watchlist.
The rule came out of a wrong claim on a live page. A real eye cream's page led with "5 legally restricted." Two of the five were PEG-20 Methyl Glucose Sesquistearate and Laureth-4, each shown as "Restricted in New York" with a citation to N.Y. Environmental Conservation Law §35-0105 / §37-0117. The citation was real, and both ingredients are legal in New York. The wrong thing was the rule value on four data entries. I corrected it there, in the one field that four parts of the product read.
No test had failed. I found it by reading the rendered page. The PEG entry's note already said New York caps 1,4-dioxane in the finished product, "not the ingredient itself," and that the ingredient's presence doesn't mean the product exceeds the limit ("only testing confirms that"). The other three indicator entries pointed back to it for the same caveat. I call this a shelving error, since the fact was right and was filed under the wrong rule value, which on that entry was restricted.
The sentence on the page came from the product's older curated-standards path, hand-encoded JSON. No model wrote it, and the newer evidence path wasn't involved.
Four Consumers Read the Same Rule Field
The New York standard is one JSON file, ny_dioxane_limits.json, with five entries. The first matches 1,4-dioxane by its CAS number, 123-91-1, and carries rule: "restricted". Its note says it fires "only on the rare label that names it directly." Manufacturers almost never declare a byproduct, so that entry almost never fires.
The other four match ingredient names. A pattern for PEG, polyethylene glycol and polyoxyethylene is one, and an "-eth-" pattern covering laureth, ceteareth, oleth and steareth is another. The third matches "-oxynol-". The last is Sodium Laureth Sulfate by CAS number. Before the fix all four carried restricted too, the closest value the vocabulary offered. Here is the "-eth-" entry as it stood (a reduction: the note is cut after its citation):
{"name": "-eth- ingredients (laureth, ceteareth, oleth, steareth, etc.)", "pattern": "LAURETH|CETEARETH|\\bOLETH\\b|STEARETH|-ETH-\\d", "rule": "restricted", "note": "N.Y. Env. Conserv. Law §35-0105 / §37-0117 ..."}Four parts of the product read that rule field, and before the fix none of them special-cased the standard it came from. standards.py builds a hit for each matched ingredient and passes rule through unchanged. passport.standard_text turns a law-origin hit into a category and a sentence, and for restricted the category is "legally restricted" and the sentence begins "Restricted in New York." The product page counts those categories into its summary line, which is where "5 legally restricted" came from. The fourth is report_matrix. It decides whether a finding occupies a market cell in the merchant report, so a restricted New York hit filled the US states cell.
source: ingredientcheck data/curated/standards/ny_dioxane_limits.json line 11 (6e07dcc^ and 6e07dcc); src/tersena/standards.py line 60; src/tersena/passport.py lines 92, 110, 165-177 (6e07dcc); src/tersena/report_matrix.py lines 78-84, 105-110; .agents/notes/implemented/bug-fix/2026-09-04-contaminant-indicator-overclaim.md lines 9, 29 · oneFieldFourConsumers.data.ts
Click RESTRICTED in the figure and the hit renders as "Restricted in New York" and the US states cell fills, which is the page as it was. Click WATCHLIST and that one entry's contribution to the summary line becomes "4 legally restricted, 1 flagged as a contamination indicator," and the hit no longer fills the US states cell. The note field is the same text in both states.
The Fix Was a Vocabulary Decision
The four indicator entries moved to rule: "watchlist". The vocabulary already had that value. A retailer standard used it for the 11 entries on its horizon list, and it meant named, but not a restriction. The 1,4-dioxane entry stayed restricted, since the statute does cap 1,4-dioxane.
The data change needed a code change too. standard_text handled every law-origin standard outside its one role-specific branch with a single line, "prohibited" if rule == "prohibited" else "restricted". A watchlist hit from New York would have fallen through to restricted and still printed "Restricted in New York." I added a branch ahead of that line for a law-origin watchlist hit. It renders a new category, contaminant_watch, shown on the page at the fix as "flagged as a contamination indicator." Its qualifier says the law caps a contaminant the ingredient can carry from manufacturing, not the ingredient itself. The hit's sentence adds that a label can't show whether the finished product is over the limit.
The merchant report needed one more line. report_matrix.py keeps a table of which rule values may occupy a market cell for each standard (a reduction: comments removed):
MATRIX_RULES: dict[str, set[str]] = { "gb_divergence": {"prohibited"}, "ny-dioxane-limits": {"prohibited", "restricted"},}The New York set leaves out watchlist, so in_matrix rejects those four findings. Only the 1,4-dioxane entry can fill a cell.
Dropping the four entries would have been the simplest fix. I kept them because an ethoxylated ingredient is a real signal that a New York limit may bind the finished product, and a merchant preparing for the statute wants to see it on the page. Keeping restricted and rewriting the sentence standard_text prints would have fixed one consumer of four. The category, the count built from it and the matrix all follow the rule value, so each would have needed a special case for one standard. A new value such as contaminant_indicator wasn't needed either, since watchlist already existed and already meant named, but not a restriction.
The decision record states the rule so the next curator can apply it without re-deriving it. An entry may be prohibited or restricted only when a label can identify the thing being restricted. When the restricted thing is something a declaration can't show, the entry is watchlist.
source: ingredientcheck .agents/notes/implemented/bug-fix/2026-09-04-recurate-drifted-standards.md line 13 (the rule), line 15; 2026-09-04-contaminant-indicator-overclaim.md lines 7, 15; src/tersena/passport.py lines 117-122 · labelCanShow.data.ts
A label shows an ingredient by name. An outright ban on an ingredient can be prohibited, and a concentration limit can be restricted with the limit stated. For a contaminant, a grade or a form, the label names the ingredient and not the regulated thing. In the figure's contaminant row, the label shows Laureth-4 and the 10 ppm cap applies to the 1,4-dioxane in the finished product.
The Same Rule Settled the Ulta and FDA Files
The next standard I re-curated, Ulta's, had a source that had drifted, and the rule decided the judgment calls. Ulta's "made without" list states two limited-use classes the file had never carried. Ethoxylated ingredients are permitted within limits: 1,4-dioxane under 10 ppm in rinse-off products and 3 ppm in leave-on, and ethylene oxide under 2 ppm in lip products and 7 ppm elsewhere. Three microbead polymers are restricted only as microbeads in rinse-off products, and the same polymers are permitted as film formers. Polyethylene on a label doesn't say whether it's a microbead. Both classes went in as watchlist, and the category's page wording was generalized at the same time, to "permitted subject to a limit a label cannot show," since neither class is a contaminant. The standard had 55 entries and now has 57.
The same rule kept a pattern out of the FDA file. The FDA's prohibited cattle materials rule, 21 CFR 700.27, names tissues. Before writing a pattern for them I measured what it would match. The knowledge base had 72 ingredients containing a tissue word, and almost none was bovine. THYMUS matched Thymus vulgaris, which is thyme, across more than 30 entries, and BRAIN, a tissue the rule does list, and NEURAL matched human and chicken cell-culture media. The rule also expressly exempts tallow (within a purity limit), tallow derivatives, hides and milk. A label doesn't say which species a tissue came from. I recorded 700.27 in the FDA file's notes field and gave it no entry. The suite had 881 tests and now has 887, and all of them pass.
None of the Other 18 Standards Had the Error
I went through all 19 curated standards looking for an indicator typed as a restriction. Counted from the files as they stood before the move, the 19 held 1,951 entries: 1,280 prohibited, 489 restricted, 171 excluded and 11 watchlist. None of the other 18 had an indicator-style entry under restricted. After the fix the same files hold 485 restricted and 15 watchlist, and nothing else changed.
Counted from the 19 curated standards files before and after the fix: 1,280 prohibited, 489 restricted, 171 excluded and 11 watchlist before; the only change is the 4 New York indicator entries. No other standard carried an indicator typed as a restriction.
source: ingredientcheck data/curated/standards/*.json, 19 standards, counted by rule at 6e07dcc^ and 6e07dcc; .agents/notes/implemented/bug-fix/2026-09-04-contaminant-indicator-overclaim.md line 32 · ruleSurvey.data.ts
The fix passed the full suite on its branch, 842 tests. The new regression tests cover standard_text, in_matrix and the report. standard_text returns contaminant_watch for a watchlist hit and still returns restricted for a real one. in_matrix rejects a watchlist finding and admits a restricted one. A report whose only finding is an indicator produces an empty jurisdiction matrix. A companion test pins the 1,4-dioxane entry at restricted, the one entry the statute covers. I re-rendered the eye cream's page from the real catalog record before and after. After the fix it read "3 legally restricted, 6 retailer or program exclusions, 2 flagged as contamination indicators."
One existing test had been asserting the error. tests/test_layers.py pinned rule == "restricted" for PEG-100 Stearate, and its comment said the rule "must never read as an outright ban." The old vocabulary's weakest law-origin value was restricted, and the assertion used it. Before (a reduction: the comment's first line is cut):
def test_ny_dioxane_indicator_ingredients_are_restricted_not_prohibited( env,): # the finished product's 1,4-dioxane ppm, not the ethoxylated # ingredient itself, so the rule must never read as an outright ban. hits = env["standards"].hits_for(_rec(env, "PEG-100 Stearate")) ny = next(h for h in hits if h.standard == "ny-dioxane-limits") assert ny.rule == "restricted" assert "ppm" in ny.noteAfter the fix, the test's name ends neither_prohibited_nor_restricted and its assertions are these (a reduction: the def line, comment and setup lines are cut). The suite's result follows, as the fix commit records it:
assert ny.rule == "watchlist" assert ny.rule not in {"prohibited", "restricted"} assert "ppm" in ny.note$ uv run --extra dev --extra ingest pytest -q842 passedTwo Law Values Could Not State a Contaminant Cap
The page overstated the law. A merchant who reads "Restricted in New York" next to a real statute citation may reformulate a product, and a shopper may throw out an eye cream. A rule field whose law-origin values were prohibited and restricted had nowhere to put the statute's own distinction between an ingredient and a contaminant, and that gap is where the overstatement came from. The Ulta file hit the same gap with microbeads, and the FDA file hit it with tissue names.
Limits
The page error is one standard and one jurisdiction, found by inspection. The survey covers the curated standards and not the evidence path, and it looked for indicator entries. Grade conditions it did not touch: Ulta's mineral oil, paraffin, petrolatum and talc entries stayed restricted through the re-curation, each with its grade or asbestos condition in the note. At the fix the widget rendered the finding as "ny dioxane limits: watchlist," accurate and terse, and friendlier wording was left as a follow-up. Whether any user saw the page before the fix isn't recorded.
If you keep a compliance dataset, the check is one pass over it. For each entry typed prohibited or restricted, ask what the label would have to name for the entry to fire, and whether that is the thing the law regulates. Run on the New York file, that pass flags four of the five entries, the ones that fire on an ingredient name.
Footnotes
-
N.Y. Environmental Conservation Law §37-0117, subdivision 3, bars selling any cosmetic product containing 1,4-dioxane at a concentration "that exceeds ten parts per million by December thirty-first, two thousand twenty-two." Subdivision 4 limits personal care products (shampoo, conditioner, soap, bath gels) to two parts per million from December 31, 2022 and one part per million from December 31, 2023. §35-0105 sets the same two and one ppm limits for household cleansing products. Each limit applies to the finished product, and none names an ingredient. The FDA's indicator syllables are guidance on where the contaminant tends to occur. The FDA states that 1,4-dioxane "is not used as an ingredient in cosmetics." The product's New York file is curated from a 2026-01-05 Wayback Machine snapshot of the state DEC's page, which returns HTTP 403 to automated fetches. ↩