Our 0–10 score has a thin middle
Of the 9,809 products in the catalog, our formula engine can score 8,794. Their scores fall into two heaps with a trench between them. In 1.0-wide bins: 3 products between 1 and 2, then 1,446 between 2 and 3, 1,866 between 3 and 4 — and then 324 between 4 and 5, and 266 between 5 and 6, before it climbs again to 938, 1,714 and 2,125 in the 6, 7 and 8 bins. Only 193 of the 8,794 (2.2%) fall in the half-point band from 4.5 up to 5.5. In total 3,315 score below 4.0 and 4,889 score 6.0 or above.
The cause is arithmetic, and we can show it rather than assert it. The score is 0.62 × Evidence + 0.38 × Formulation. Evidence takes only 80 distinct values across all 8,794 products, and three sub-ranges of it are literally unreachable: 0 products have Evidence between 2.9 and 3.5, 0 between 4.7 and 5.0, 0 between 6.7 and 7.0. Evidence is a tier weight (7.0 strong, 5.0 moderate, 3.5 emerging, 2.0 supportive) multiplied by a position factor (1.1, 1.0, 0.5 or 0.25) and a dose factor (1.2, 1.0 or 0.5), plus 8% of the next three actives — a lattice, not a continuum. 3,018 products sit at Evidence 2.0 or below, 1,649 of them pinned at the 1.0 floor the scorer assigns when it detects no active at all; 4,149 sit at 7.0 or above; only 1,627 are strictly in between.
So a reader who assumes 5 means average, and that 6.2 beats 6.0 by a little, is reading the number wrong. It is really answering a near-binary question — is there a well-evidenced active sitting high enough in the published list to count, or isn't there — and that question has almost no in-between answers. Use it to sort products into two piles, not to split hairs. And note what this is not: a differently-designed scorer would produce a different shape from the same 9,809 lists. It says our instrument has almost no vocabulary for 'medium'. It does not say mid-quality products are rare.
Products by Formula Score, in whole-point bins. The 4–5 bin is the trench.
"Weak" is mostly a report of an absence
Weak is our biggest band: 3,124 of the 8,794 scored products, against Good 2,895, Fair 1,689, Excellent 571 and Mostly Marketing 515. In 1,538 of those 3,124 (49.2%) the engine graded no active whatsoever — the actives list is empty.
Across all bands, 1,888 products have no graded active, 1,471 of them not scored as cleansers. They sit at the Evidence floor the scorer assigns when it recognises nothing, which caps the score by arithmetic — and they land in Weak rather than Mostly Marketing, because an explicit floor keeps a product with no marketed hero out of the harsher label.
Our two low bands therefore mean opposite things. Weak is mostly "nothing was claimed". Mostly Marketing is "something was claimed and it sits at the bottom of the list": of its 515 products, 502 do carry at least one graded active, and in all 502 every one of those actives sits at or below the estimated 1% line — which is the band's own entry condition, not a discovery, and we offer it as an explanation of the label rather than as evidence.
A checkable example: CeraVe Daily Moisturizing Lotion (catalog id 657) scores 3.3, bands Weak, has Tolerability 9.0 — the engine's maximum — 24 ingredients and zero graded actives, while the same engine still recognises ceramides and hyaluronic acid in that list and shows them as notables. Its list was read from cerave.com on 19 June 2026. "No graded active" is a limit of our grading, not a fault in a product that never claimed to have one.
The five bands across the 8,794 products the engine scores.
The big number says nothing about how gentle a product is
Across the 8,794 scored products, the score correlates 0.9779 with Evidence, 0.6903 with Formulation and −0.0453 with Tolerability. Mean Tolerability by score decile, lowest to highest, runs 6.79, 6.80, 6.74, 6.63, 6.92, 6.62, 6.40, 6.50, 6.59, 6.63 — a 0.52-point band across the entire scale. That is by construction: the score is 0.62 × Evidence + 0.38 × Formulation, and Tolerability is not an input.
Tolerability is 9.0 minus five composition flags — fragrance −3.0, aromatic botanical −2.5, drying alcohol −2.5, high-strength acid −1.5, EU-annex ingredient above the 1% line −1.0. Reconstructing it that way reproduces the dumped value on all 8,794 rows with zero mismatches, so 9.0 is the starting value and the maximum, not an observed high. It reaches the output in one place only: below 4.0 it caps the band at Fair. 558 products show a band below their computed base band; 485 of those are this cap (411 Good→Fair, 74 Excellent→Fair) and the other 73 are a separate under-dosed-hero cap.
That produces a trap in our own labels. Fair holds 1,689 products, and it is consequently the least gentle band we have: mean Tolerability Fair 5.72, against Mostly Marketing 6.38, Weak 6.80, Good 7.02, Excellent 7.16. A Fair product is on average less gentle than a Weak one.
One thing the arithmetic forbids: no single flagged class can ever downgrade anything. The largest single deduction is fragrance's 3.0, so one flag alone lands at 6.0 and the 4.0 cap is out of reach. 3,552 of the 8,794 carry the fragrance flag, 2,600 of them sit at Tolerability 4.0 or above, and 957 band Good or Excellent. 989 products fall below the 4.0 line in total.
Which word the label prints predicts what you can find out
Every one of the 9,809 catalog products has a stored ingredient list, so we re-applied the scorer's own two-word fragrance rule to all of them ourselves, validating the rebuild against the engine's own flags on the 8,794 it scores: 8,794 of 8,794 exact, zero mismatches. 4,052 of the 9,809 declare fragrance.
Classify those 4,052 by the raw token the list prints. 2,880 print "Parfum" somewhere — alone (553), or as "Parfum (Fragrance)" (919), "Fragrance (Parfum)" (578), "Parfum/Fragrance" (477). 1,172 print only "Fragrance". Then ask whether the list also names at least one of 25 of the EU's 26 declarable fragrance allergens (we exclude benzyl alcohol, which is overwhelmingly a preservative in these lists). The Parfum group: 1,332 of 2,880, or 46.3%. The Fragrance-only group: 167 of 1,172, or 14.1%. Rate ratio 3.25, two-proportion z = 19.1. It is not just brand mix: across the 46 brands with at least three fragranced products under each convention (2,011 products), it is 36.7% against 13.3%.
The obvious explanation is a border rather than candour. "Parfum" is the EU-standard INCI term and "Fragrance" the US-convention one, and EU labelling requires naming those allergens above a threshold while US labelling does not. But that is an inference we cannot verify from this data: 97.5% of these lists were read from incidecoder.com, a region-unmarked aggregator, and only one of the 4,052 source URLs is an explicitly EU page. Twenty-four of the brands in the within-brand check go the other way, and four are level.
A neutral pair, both read from incidecoder.com on 16 July 2026, both scoring 8.8 and banded Excellent today: Olay Vitamin C + AHA24 Serum (id 10476, incidecoder's EU page) lists Parfum plus Limonene, Linalool and Citronellol; Olay Vitamin C + Peptide 24 Brightening Serum (id 10826) lists Fragrance and names no allergen. If you have been patch-tested and told to avoid linalool, look for the EU listing of a product before concluding it is hiding anything — and read a blank as a blank, because a product naming no allergen may contain none above the threshold.
A checkable claim beats a mood word
Searching the names of all 9,809 catalog products for "fragrance-free", "fragrance free", "unscented" or "without fragrance" returns 39 — 31 the formula engine scores and 8 sun-category products it never runs. Applying the scorer's vocabulary to all 39 stored lists: 0 declare a fragrance term, 0 hit the nine-term aromatic-botanical list, 0 name any of 25 of the EU's 26 declarable fragrance allergens. Four of the 39 do list the 26th, benzyl alcohol, sitting among the preservatives. Against a base rate of 41.1% — 4,052 of the 9,869 products making no such claim declare a fragrance term — 39 out of 39 clean is not chance.
The softer vocabulary behaves differently, and not because it is dishonest. Of the 757 catalog products named around calm, soothing, cica, sensitive or redness, 299 (39.5%) carry a fragrance or aromatic-botanical flag, against 57.1% of the other 9,151. That is not a category-mix artefact — matching each of the 757 to its own primary category's rate among non-claiming products gives an expectation of 58.9% — but 39.5% is still two products in five, and none of those words was ever a fragrance-free claim in the first place.
The honest reading of this section is narrow. Thirty-nine products come from 14 of our 255 brands, and 23 of the 39 are Olay or Neutrogena: this is two US mass brands' fragrance-free lines matching their published lists, not a survey of a segment. Our own word lists are narrow too — the aromatic-botanical flag is nine substrings and never looks for rose, tea tree, frankincense or jasmine, and two of the 39 do list an aromatic botanical outside it (Boswellia, Rosmarinus). What survives is the general point: a specific claim is checkable against a published list in thirty seconds, and an adjective is not.
"EU restricted" mostly means the EU permits it
2,207 of the 8,794 scored products (25.1%, or 22.5% of the full 9,809) carry an "EU restricted" note. Of those notes, 1,052 cite Annex III (restricted with limits), 393 Annex IV (approved colourants), 79 Annex VI (approved UV filters) and 90 titanium dioxide, which is on both — 1,614 in total, 73.1%, naming an ingredient the EU lists as permitted. 137 distinct ingredients generate all 2,207 notes, and the top four are salicylic acid (291), potassium hydroxide (215), kaolin (185) and polyacrylamide (112): an exfoliating acid, a pH adjuster, a clay and a thickener. The scorer never consults the annex — it charges a flat 1.0 gentleness point for any entry and breaks after the first hit — so we flattened five different lists into one badge.
The note cannot lower the Formula Score at all, and products carrying one in fact average higher: 6.42 against 5.56. That is an association in this catalog, not an effect — flagged products resolve more ingredients on average, 31.5 against 29.6.
The one annex that really is a prohibition list, Annex II, produces a note on 302 of the 8,794 across 96 brands: petrolatum 84, citrus materials the bulk of the rest. On that same card our app prints "Annex II — banned in cosmetics in the EU" and, four lines below, "not because the ingredient is banned." Our own dictionary holds the qualifier verbatim — the petrolatum entry is written as an exception — but our note class has no field to carry it, so no condition reaches the screen. That one is still true and still unfixed.
Fixed since this page first said it. The version of this report published on 5 September said 195 of these notes had no annex at all, and traced every one to three dictionary rows whose restriction cell held something that was not a restriction: Palmitoyl tripeptide-1 stored an IUPAC name, Beeswax an EC number, Palmitoyl oligopeptide a substance definition. Beeswax was being flagged as EU-restricted because of its EC number. Twenty-one such rows were cleared in catalog v172 and v173, and the count of annex-less notes is now zero. We are leaving the paragraph here rather than deleting it, because a page that only ever reports its own defects in the present tense is not being honest about how they get fixed.
Sunscreen is where reading the list stops working
The catalog holds 871 sun-category products; none of them is formula-scored, so none sits in the 8,794 above. 810 now show a Protection number, up from 634 when this page was first published — the difference is 176 labels read and confirmed one at a time. 787 of the 810 name at least one of the 27 UV filters in our vocabulary.
Group those 787 by the exact set of filter molecules named and you get 283 distinct sets — of which 44 hold more than one distinct checked SPF. The largest is avobenzone, ethylhexyl salicylate, homosalate and octocrylene with nothing else we recognise: 79 products carrying that identical filter set are labelled SPF 15, 20, 25, 30, 35, 40, 45, 50, 60, 70 and 100. Higher SPF from the same filters is exactly what more filter buys — the point is that an ingredient list declares order, never amount, so it cannot show you which is which.
The number on the front does not close the gap either. 522 of the 810 record SPF 50, and they split into twelve different Protection reads — 207 at "at least 55 of 70", 120 at "70 of 100", 82 at "30 of 35", 60 at "25 of 25", and eight more. Every point of that spread is UVA evidence the label either carried or did not: a PA grade on 289, the EU UVA circle on 155, US "Broad Spectrum" on 194, a measured UVA-PF on 12, Boots stars on 1, and nothing stated at all on 159. Only 157 of the 871 have all four Protection rows assessed.
The measured rung was supposed to be empty. Our own plan called a measured UVA-PF "the strongest input" and said none was obtainable. Twelve products publish one — several as a photographed in-vivo certificate on the brand's own page, read at magnification: UVA-PF 8.0 through 52.3, two of them with a critical wavelength (371 nm and 374 nm). Where a brand published two of its own figures, we store the lower one.
Only 61 of the 871 now show a dash instead of a number, down from 336. 53 of those are about us — nobody has read that label yet, and for a large share of them nobody can, because the product is discontinued and the brand has taken the page down. The remaining 8 are withheld through a single gate: each names 4-methylbenzylidene camphor, the one filter our table flags as not permitted in EU cosmetic products. A dash is not a bad grade; it means we do not know.
One limit worth stating plainly: 26 of the 871 publish a list in which our 27-name vocabulary finds no UV filter at all. Rather than run its tolerance check over an empty filter list and quietly award full marks, the engine drops their Gentleness denominator and says so.
And a provenance limit we will not dress up: 36% of the verified labels were read from an ingredient database or a web archive rather than from the brand — 219 from one aggregator, 73 from the Wayback Machine. Every label read in this round came off a brand domain, which is why the share improved from 55%, but the older rows are still there and still counted above.
We were docking products a point for telling the truth
Until 5 September 2026 our scorer deducted 1.0 point of Tolerability when an ingredient list named a fragrance allergen — the disclosure EU labelling asks for. 1,956 of the 8,794 scored products (22.2%) carry that flag and paid the point. Reconstructing the tolerability model from the flags reproduces the dumped value on all 8,794 rows with zero mismatches, so the arithmetic below is exact rather than estimated.
Add the removed point back and 61 of those 1,956 (3.12%) cross back over the 4.0 line and out of the Fair cap: 51 products banded Good today and 10 banded Excellent were being held at Fair. The 0–10 Formula Score never moved in either version — Tolerability is not one of its inputs. And the point was a tipping point, never a sole cause: all 61 already carried 4.5 (29 of them) or 5.0 (32) of deductions from two or three other flagged classes, and none had the disclosure point as its only deduction. The fair comparison class is the 139 products sitting in the same 4.0–4.9 Tolerability window and the same Good-or-Excellent bands without a named allergen, which kept their band.
The same-brand illustration from earlier makes it concrete. Both Olay serums score 8.8 and band Excellent today, both lists were read on 16 July 2026. Under the old arithmetic, id 10476 — Parfum plus Limonene, Linalool and Citronellol — would have gone to Tolerability 3.5 and been capped at Fair; id 10826, which prints Fragrance and names no allergen, would not. Naming them is a neutral illustration of our bug, not a judgement of either product.
Any rating that deducts for a named ingredient quietly rewards the vaguer label, because the way to lose the deduction is to say less. The flag still fires and still shows on the product page, because a reader with a known allergy needs to see it; what we removed is the arithmetic. And the same arithmetic still ships in one place: SunScore's fragrance component awards 18 of 25 rather than 25 of 25 when a list names an allergen and shows nothing else aromatic, which today affects 6 of our 970 sun products.
Our own scorer could not place salicylic acid — and now can
This section reported a live defect when it was first published. It has been fixed. We are leaving it in, corrected, because a report that quietly deletes its own findings once they are dealt with teaches the reader nothing about whether anything gets dealt with.
What was wrong: our engine's one free check — is the active above or below the estimated 1% line — was structurally broken for one common ingredient. Salicylic acid is a listed EU preservative and our only strong-tier BHA, and the scorer drew its trace line at the first preservative-coded ingredient. So on a 2% BHA exfoliant the line was drawn on the product's own active, which then read as buried beneath it. It fired on 383 of 936 detections. Only 43 were ever recorded above the line, and 37 of those were Capryloyl Salicylic Acid — a different molecule our substring rule catches. The engine could not say a BHA was dosed.
The fix is a rule written on the property rather than the molecule: an ingredient the scorer itself grades as an active can no longer mark the trace line, and the scan continues so that a real preservative further down still sets it. Across the same 936 detections, 0 now set the line and 430 (46%) are read as above it, none of them by the substring accident. The products that gained most were the ones the defect hit hardest: a 2% BHA exfoliant went from 3.80 and a Weak band to 8.70 and Excellent. Its formula never changed; only our ability to read it did. Salicylic acid is now the single most common ingredient behind an EU-annex note in the catalog, at 291 products, because we can finally see where it sits.
Two honest caveats. The direction of the old error was conservative — salicylic acid works at around 0.5% and often genuinely does sit below 1% — so "not above the line" was materially right in many of the 383; what was wrong is that the engine could never say otherwise. And 7 products still carry our "named but buried" note for salicylic acid, which is now a real reading rather than an artefact of the line.
What this page cannot show
This catalog is what we chose to ingest, not a sample of the market. Every figure on this page is over 9,809 specific products from 255 brands that we added to Luni, and nothing here supports a sentence beginning "of skincare". Where a figure is over the 8,794 products our formula engine can score, we have said so in the sentence itself, because that subset is not the catalog: the other 1,015 products all have ingredient lists, they simply carry no score, no band, no flags and no actives in our dump. Their absence from a count is never a zero.
We have never read a pack. Every ingredient list here came off a published web page on a recorded date, between 15 June and 2 September 2026, and 9,103 of the 9,809 came from one host, incidecoder.com — a third-party aggregator that does not mark which market a listing belongs to. That single dependency shapes several findings: it is why we cannot tie any list to the EU or the US, and it is why the Parfum-versus-Fragrance section is a correlation between a label term and a label practice rather than a demonstration about regulation. A brand may have reformulated since the date on its row, and we would not know.
We cannot read concentration. Our engine detects 19,461 actives across the 8,794 scored products and records no view at all on the dose of 19,120 of them — 98.2%. Only 306 products (3.5%) contain even one active whose dose we can judge. More than half that gap is ours rather than the brands': our own table holds no working dose for many of those actives to compare a number against, because whole classes — peptides, antioxidants, UV filters, growth factors — deliberately carry none. Everything else rests on ingredient order, which regulation requires only down to 1%; below that line the order is arbitrary. On 468 of the 8,794 products no 1%-marker is found at all, so every active detected in them is treated as above the line by default.
Our recognition vocabulary is small and its blind spots are ours. The actives table is 78 substring rules resolving to 222 identities; the fragrance-allergen flag reads 8 substrings where the EU names 26; the aromatic-botanical flag is 9 substrings that never look for rose, tea tree, frankincense or jasmine; the UV filter vocabulary is 27 names; the 1% line is located by 20 hard-coded markers. The clearest illustration is in our own most-detected active: the rule reads "tocopherol" and not "tocopheryl", so 1,241 further scored products that print only the ester spelling get no Evidence credit for vitamin E at all. An ingredient we hold no rule for is invisible here, and every "we found none" on this page means "our vocabulary found none".
A flag is not a verdict and a missing value is not a finding. Tolerability is a composition estimate read off a published list — it is a cosmetic-suitability signal, never a prediction that any individual will react to anything, and never a safety statement. "EU restricted" is a list-membership fact; on 76.3% of our own notes it names something the EU permits. A dash on a sunscreen means nobody has checked that label yet, in 328 of 336 cases. A product that prints a fragrance term and names no allergen may contain none above the threshold. And every band on this page is our engine's opinion about a formula, produced with no user profile attached, so the stricter personalised cap for sensitised skin never fires in any figure quoted here.
Finally, three of the nine sections above are defects in our own instrument rather than findings about products, and we would rather you read them that way: the disclosure penalty we removed, the dictionary rows holding a chemical identifier where a restriction should be, and the salicylic acid line-setting bug. All three are now fixed; each section says so and shows the before and after. The counts throughout are as of the 8 September 2026 dump over catalog v175.
How the numbers were produced
Every number here was produced by running Luni's own scoring engine — not SQL, and not a summary table — over all 9,809 products in catalog version skinlogic_v175, using the harness at tool/dump_catalog_index_rows.dart, which emits one JSON row per product (id, brand, name, category, flags, score, band, evidence, formulation, tolerability, actives, euRestricted, sun, source URL and fetch date). That dump was retaken on 8 September 2026 and every figure on this page was then recomputed from it in Python, with the raw catalog at seed/skinlogic.db opened read-only for the ingredient lists and the ingredient dictionary. Mechanisms are quoted from source — lib/core/scoring/formula_scorer.dart, formula_score.dart and sun_score.dart — and where we relied on a reconstruction we validated it against the engine's own output first: the tolerability model reproduces the dumped value on 8,794 of 8,794 scored rows, the fragrance-flag rebuild over all 9,809 stored lists matches the engine's flags on 8,794 of 8,794, the band-cap model reproduces the shipped band on 8,794 of 8,794, the restricted-note loop reproduces the shipped note on 8,794 of 8,794, and the ported sunscreen Protection builder reproduces all 634 published reads with zero mismatches. Anyone with the repository can re-run the harness and the counts; the product ids cited as examples are catalog ids and each carries its own source URL and fetch date.