on-notice 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Waiga Arya
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,245 @@
1
+ Metadata-Version: 2.4
2
+ Name: on-notice
3
+ Version: 0.1.0
4
+ Summary: Which cosmetic ingredients have an EU deadline that has not arrived yet
5
+ Author: Waiga Arya
6
+ License: MIT
7
+ Keywords: cosmetics,regulation,eu,inci,eur-lex
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Manufacturing
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Utilities
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Dynamic: license-file
23
+
24
+ # On Notice
25
+
26
+ Reads a cosmetic ingredient list and reports which ingredients on it are named in
27
+ an EU rule that carries a date still ahead of it, and what that date is.
28
+
29
+ It does not say whether a product may be sold. It reports dates.
30
+
31
+ ## Why this one exists
32
+
33
+ The other tools in this set check what is wrong with a document today. A cosmetic
34
+ reformulation takes the better part of a year, so the question that actually costs
35
+ money is the other one: what becomes wrong while the stock is still on the shelf.
36
+
37
+ Regulation (EU) 2026/909 was adopted on 27 April 2026. It restricts Benzyl
38
+ Salicylate in fragrance products to 4 per cent, with lower limits in eight other
39
+ product types, and the date it bites is 1 January 2027. A product on a shelf
40
+ today can be entirely lawful and already be on a clock, and nothing on the pack
41
+ says so.
42
+
43
+ ## Use
44
+
45
+ ```bash
46
+ on-notice ingredients.txt
47
+ on-notice --unresolved # everything the register could not settle
48
+ cat list.txt | on-notice -
49
+ ```
50
+
51
+ Exit code is 1 when something on the list has a date, 0 when nothing does, 2 when
52
+ the register cannot be read.
53
+
54
+ ```
55
+ on-notice 0.1.0 register built 2026-09-18
56
+ read 5 ingredient(s) from night-cream.txt
57
+
58
+ 2 of them are named in an EU rule with a date still ahead of it.
59
+
60
+ 1 January 2027 products that do not meet the new restriction cannot be placed on the Union market
61
+ Benzyl Salicylate
62
+ on the label as Benzyl Salicylate
63
+ named in Commission Regulation (EU) 2026/909, Annex III entry 75
64
+ the rule says From 1 January 2027 cosmetic products containing these substances and not complying with the restrictions shall not be placed on the Union market.
65
+ ```
66
+
67
+ The register ships inside the package. Nothing leaves your machine.
68
+
69
+ ## What it was measured against
70
+
71
+ The corpus is the Open Beauty Facts CSV export, `en.openbeautyfacts.org.products.csv.gz`,
72
+ the copy generated on 18 September 2026 at 00:22 GMT, 64,237 product rows.
73
+ `scripts/measure_corpus.py` reproduces every number below from it. 17,626 of those
74
+ rows carry an ingredient list. 31 of those lists yielded no readable ingredient
75
+ names at all, leaving **17,595 lists read**, median 17 ingredients each. Nothing
76
+ in it was written by this tool or by anybody connected to it.
77
+
78
+ **3,050 of the 17,595, or 17.3 per cent, name at least one substance carrying a
79
+ date that has not arrived.** 2,703 name one such substance, 334 name two, 13 name
80
+ three.
81
+
82
+ Counted by the number of lists naming each:
83
+
84
+ | Substance | Lists | Date | What the date is |
85
+ |---|---:|---|---|
86
+ | Benzyl Salicylate | 1,614 | 1 January 2027 | new restriction starts |
87
+ | Citral | 1,416 | 1 January 2027 | new restriction starts |
88
+ | Diethylamino Hydroxybenzoyl Hexyl Benzoate | 190 | 1 January 2027 | new restriction starts |
89
+ | Pinene | 77 | 1 August 2028 | already in force, sell-through ends |
90
+ | Methyl Salicylate | 56 | 1 August 2028 | already in force, sell-through ends |
91
+ | Retinol | 47 | 1 May 2027 | already in force, sell-through ends |
92
+ | Zinc Acetate | 4 | 1 January 2027 | new restriction starts |
93
+ | Triphenyl Phosphate | 3 | 1 January 2027 | prohibition starts |
94
+ | Geranial | 3 | 1 January 2027 | new restriction starts |
95
+
96
+ Nine of the register's fourteen substances were seen at least once. The other five
97
+ appear in no list it read, which is a fact about this corpus and not evidence that
98
+ nothing uses them.
99
+
100
+ **The corpus is not a sample of the EU market.** Open Beauty Facts is a global,
101
+ volunteer-contributed database with a heavy western European skew. It is what was
102
+ available and reproducible, not a representative frame, and the percentage above
103
+ should be read as a property of that corpus rather than of the market the rules
104
+ govern.
105
+
106
+ ## The first number this tool produced was wrong three times, each time in its own favour
107
+
108
+ The first complete run reported **44.0 per cent**. Then **18.6 per cent**. The
109
+ number that survived scrutiny is **17.3 per cent**. Every correction moved it
110
+ down, which is to say every error made the tool look more impressive than it was.
111
+
112
+ **First error: it counted compliance as exposure.** Around fifty of the substances
113
+ in the first register came from Regulation (EU) 2023/1545, titled "as regards
114
+ labelling of fragrance allergens". Quoted from Annex III as it amends it:
115
+
116
+ > The presence of the substance shall be indicated in the list of ingredients
117
+ > referred to in Article 19(1), point (g), when its concentration exceeds:
118
+ > 0,001 % in leave-on products, 0,01 % in rinse-off products.
119
+
120
+ The duty is to **name the substance in the list of ingredients**. So when a tool
121
+ that reads printed ingredient names found Limonene on 5,301 labels, it had found
122
+ 5,301 products doing the thing the rule asks for. The products actually at risk
123
+ print only `parfum` and never name the allergen, and no check that reads printed
124
+ names can see a single one of them. The finding pointed the opposite way from
125
+ what a reader would have assumed.
126
+
127
+ **Second error, fixing the first: it threw away a real restriction.** Annex III
128
+ entry 75 as replaced by Regulation 2026/909 carries **both** a 4 per cent maximum
129
+ in fragrance products and the labelling sentence. A first fix matched the
130
+ labelling phrase and filed the whole entry as a labelling duty, discarding a real
131
+ restriction on 1,614 products. Same error, sign reversed.
132
+
133
+ **Third error: the fix had a hole big enough for ten entries.** The strip matched
134
+ only "The presence of **the substance** shall be indicated". Many entries read
135
+ "The presence of the substance **or substances** shall be indicated **as
136
+ 'Eucalyptus Globulus Oil'**". Those were never stripped, so the 0,001 per cent
137
+ labelling threshold inside the sentence was read as a concentration limit and ten
138
+ labelling-only entries shipped as findings. One of them, Eucalyptus Globulus Leaf
139
+ Oil at 94 lists, was the fourth row of an earlier draft of the table above.
140
+
141
+ Entries are now classified from their own text, three ways rather than two. The
142
+ labelling sentence is removed; a maximum concentration or permitted product type
143
+ surviving that removal makes it a restriction on use. An entry in Annex II is a
144
+ prohibition and needs no conditions column. An entry whose restriction text was
145
+ never recovered is reported as neither, because promoting it invents a finding
146
+ and dropping it hides a date.
147
+
148
+ A separate defect from the same audit: the chemical name
149
+ `1-(2,6,6-trimethyl-2-cyclohexen-1-yl)-2-buten-1-one (16)` had its trailing
150
+ bracket split off as a synonym, producing a matchable substance named `16`. Any
151
+ label carrying a bare 16 would have matched it.
152
+
153
+ ## What it cannot tell you
154
+
155
+ **It cannot tell you whether your product complies.** Every restriction here
156
+ catches products that do not meet a new condition, and those conditions are
157
+ concentrations and product types. A printed ingredient list states no
158
+ concentrations. The tool can tell you a date applies to an ingredient you use. It
159
+ cannot tell you which side of it you are on. The rule text is printed with every
160
+ finding so you can check it yourself.
161
+
162
+ **Some conditions are not formulation choices at all.** The 1 January 2027
163
+ condition on Diethylamino Hydroxybenzoyl Hexyl Benzoate is a limit of 10 ppm on a
164
+ trace impurity in the filter you buy. Several entries turn on a peroxide value.
165
+ Those are answered by a supplier certificate, not by a recipe.
166
+
167
+ **Some of these rules are already in force.** Regulation 2023/1545's placing limb
168
+ passed on 31 July 2026 and Regulation 2024/996's on 1 November 2025. What remains
169
+ is the sell-through window. Seven of the 21 shipped entries are in that state and
170
+ are labelled as such on every run, because calling a rule already in force "not
171
+ yet applying" is simply false.
172
+
173
+ **It cannot see the products at risk under a labelling rule.** 50 substances carry
174
+ a dated duty to be named on the pack above a threshold. A list naming one is
175
+ meeting that duty. The list that omits it is the one at risk and is invisible
176
+ here. These are reported separately on every run and never counted.
177
+
178
+ **The percentage understates in one direction as well as overstating in the
179
+ other.** Annex III entry 379 as inserted by Regulation 2026/909 covers
180
+ "Aluminium-containing ingredients with the exception of those listed in entries
181
+ 34, 50, 189, 190 and 192 of Annex III...", with the same 1 January 2027 date.
182
+ There is no honest way to match a class with an exception list against a printed
183
+ name, so it is held back. Aluminium compounds are among the most common things in
184
+ antiperspirants and colour cosmetics, so the true number of products in this
185
+ corpus carrying a 1 January 2027 obligation is materially higher than 17.3 per
186
+ cent. Both sentences belong with the number.
187
+
188
+ **Three entries had no restriction text recoverable.** Several Annex III entries
189
+ print across multiple rows with the conditions on a parent row. Those three are
190
+ reported and never matched.
191
+
192
+ **One sentence was not interpreted.** A sentence mentioning a date and the Union
193
+ market in wording the builder does not recognise is recorded verbatim rather than
194
+ guessed at. `on-notice --unresolved` prints it.
195
+
196
+ **The register is a scan, not the law.** It covers Commission regulations amending
197
+ Regulation (EC) No 1223/2009 from 2013 onward, read from EUR-Lex: 52 listed, 52
198
+ read, 0 that could not be downloaded. 120 dated provisions found, 703 substance
199
+ links made, of which 21 are dated after the build and shipped. 9 provisions could
200
+ not be tied to named substances; every one has a date already in the past, so none
201
+ reaches the shipped register. National rules, rules outside the Cosmetics
202
+ Regulation, and anything adopted after the build date are outside it.
203
+
204
+ **A quiet answer is not a clean bill.** The tool prints what it could not see on
205
+ every run, including runs that find nothing.
206
+
207
+ ## How the register is built
208
+
209
+ `scripts/build_register.py` reads each regulation from EUR-Lex, parses the annex
210
+ tables, links footnote markers to the entries carrying them, and extracts the
211
+ dates. Four things about those documents broke a first attempt, all silently:
212
+
213
+ - Regulation 2023/1545 states its deadline as "may be placed on the Union market
214
+ until 31 July 2026", not as "shall not be". A parser looking only for the
215
+ negative form reports nothing for every substance in it.
216
+ - A footnote can put two dates and two restrictions in consecutive sentences. A
217
+ pattern crossing the sentence boundary pairs the first date with the second
218
+ restriction, and every sell-through date comes out wrong.
219
+ - EUR-Lex serves two different markups for footnotes and prints the same marker
220
+ three ways.
221
+ - Annex attribution reads from lead-in wording that takes several forms, so
222
+ entries were filed under the wrong annex while looking entirely correct.
223
+
224
+ One fetch returned HTTP 200 with an empty body, and the build reported it as a
225
+ regulation read with no deadlines in it, which is indistinguishable from the
226
+ truth. Responses are now checked for Official Journal markup before being
227
+ accepted, and a bad one is recorded as a failure rather than cached.
228
+
229
+ `docs/how-it-reads-the-source.md` records what these documents actually look like.
230
+
231
+ ## Install
232
+
233
+ Not published to the package index.
234
+
235
+ ```bash
236
+ git clone https://github.com/Waiga/on-notice
237
+ cd on-notice
238
+ pip install -e .
239
+ ```
240
+
241
+ Python 3.9 or later. No dependencies.
242
+
243
+ ## Licence
244
+
245
+ MIT.
@@ -0,0 +1,222 @@
1
+ # On Notice
2
+
3
+ Reads a cosmetic ingredient list and reports which ingredients on it are named in
4
+ an EU rule that carries a date still ahead of it, and what that date is.
5
+
6
+ It does not say whether a product may be sold. It reports dates.
7
+
8
+ ## Why this one exists
9
+
10
+ The other tools in this set check what is wrong with a document today. A cosmetic
11
+ reformulation takes the better part of a year, so the question that actually costs
12
+ money is the other one: what becomes wrong while the stock is still on the shelf.
13
+
14
+ Regulation (EU) 2026/909 was adopted on 27 April 2026. It restricts Benzyl
15
+ Salicylate in fragrance products to 4 per cent, with lower limits in eight other
16
+ product types, and the date it bites is 1 January 2027. A product on a shelf
17
+ today can be entirely lawful and already be on a clock, and nothing on the pack
18
+ says so.
19
+
20
+ ## Use
21
+
22
+ ```bash
23
+ on-notice ingredients.txt
24
+ on-notice --unresolved # everything the register could not settle
25
+ cat list.txt | on-notice -
26
+ ```
27
+
28
+ Exit code is 1 when something on the list has a date, 0 when nothing does, 2 when
29
+ the register cannot be read.
30
+
31
+ ```
32
+ on-notice 0.1.0 register built 2026-09-18
33
+ read 5 ingredient(s) from night-cream.txt
34
+
35
+ 2 of them are named in an EU rule with a date still ahead of it.
36
+
37
+ 1 January 2027 products that do not meet the new restriction cannot be placed on the Union market
38
+ Benzyl Salicylate
39
+ on the label as Benzyl Salicylate
40
+ named in Commission Regulation (EU) 2026/909, Annex III entry 75
41
+ the rule says From 1 January 2027 cosmetic products containing these substances and not complying with the restrictions shall not be placed on the Union market.
42
+ ```
43
+
44
+ The register ships inside the package. Nothing leaves your machine.
45
+
46
+ ## What it was measured against
47
+
48
+ The corpus is the Open Beauty Facts CSV export, `en.openbeautyfacts.org.products.csv.gz`,
49
+ the copy generated on 18 September 2026 at 00:22 GMT, 64,237 product rows.
50
+ `scripts/measure_corpus.py` reproduces every number below from it. 17,626 of those
51
+ rows carry an ingredient list. 31 of those lists yielded no readable ingredient
52
+ names at all, leaving **17,595 lists read**, median 17 ingredients each. Nothing
53
+ in it was written by this tool or by anybody connected to it.
54
+
55
+ **3,050 of the 17,595, or 17.3 per cent, name at least one substance carrying a
56
+ date that has not arrived.** 2,703 name one such substance, 334 name two, 13 name
57
+ three.
58
+
59
+ Counted by the number of lists naming each:
60
+
61
+ | Substance | Lists | Date | What the date is |
62
+ |---|---:|---|---|
63
+ | Benzyl Salicylate | 1,614 | 1 January 2027 | new restriction starts |
64
+ | Citral | 1,416 | 1 January 2027 | new restriction starts |
65
+ | Diethylamino Hydroxybenzoyl Hexyl Benzoate | 190 | 1 January 2027 | new restriction starts |
66
+ | Pinene | 77 | 1 August 2028 | already in force, sell-through ends |
67
+ | Methyl Salicylate | 56 | 1 August 2028 | already in force, sell-through ends |
68
+ | Retinol | 47 | 1 May 2027 | already in force, sell-through ends |
69
+ | Zinc Acetate | 4 | 1 January 2027 | new restriction starts |
70
+ | Triphenyl Phosphate | 3 | 1 January 2027 | prohibition starts |
71
+ | Geranial | 3 | 1 January 2027 | new restriction starts |
72
+
73
+ Nine of the register's fourteen substances were seen at least once. The other five
74
+ appear in no list it read, which is a fact about this corpus and not evidence that
75
+ nothing uses them.
76
+
77
+ **The corpus is not a sample of the EU market.** Open Beauty Facts is a global,
78
+ volunteer-contributed database with a heavy western European skew. It is what was
79
+ available and reproducible, not a representative frame, and the percentage above
80
+ should be read as a property of that corpus rather than of the market the rules
81
+ govern.
82
+
83
+ ## The first number this tool produced was wrong three times, each time in its own favour
84
+
85
+ The first complete run reported **44.0 per cent**. Then **18.6 per cent**. The
86
+ number that survived scrutiny is **17.3 per cent**. Every correction moved it
87
+ down, which is to say every error made the tool look more impressive than it was.
88
+
89
+ **First error: it counted compliance as exposure.** Around fifty of the substances
90
+ in the first register came from Regulation (EU) 2023/1545, titled "as regards
91
+ labelling of fragrance allergens". Quoted from Annex III as it amends it:
92
+
93
+ > The presence of the substance shall be indicated in the list of ingredients
94
+ > referred to in Article 19(1), point (g), when its concentration exceeds:
95
+ > 0,001 % in leave-on products, 0,01 % in rinse-off products.
96
+
97
+ The duty is to **name the substance in the list of ingredients**. So when a tool
98
+ that reads printed ingredient names found Limonene on 5,301 labels, it had found
99
+ 5,301 products doing the thing the rule asks for. The products actually at risk
100
+ print only `parfum` and never name the allergen, and no check that reads printed
101
+ names can see a single one of them. The finding pointed the opposite way from
102
+ what a reader would have assumed.
103
+
104
+ **Second error, fixing the first: it threw away a real restriction.** Annex III
105
+ entry 75 as replaced by Regulation 2026/909 carries **both** a 4 per cent maximum
106
+ in fragrance products and the labelling sentence. A first fix matched the
107
+ labelling phrase and filed the whole entry as a labelling duty, discarding a real
108
+ restriction on 1,614 products. Same error, sign reversed.
109
+
110
+ **Third error: the fix had a hole big enough for ten entries.** The strip matched
111
+ only "The presence of **the substance** shall be indicated". Many entries read
112
+ "The presence of the substance **or substances** shall be indicated **as
113
+ 'Eucalyptus Globulus Oil'**". Those were never stripped, so the 0,001 per cent
114
+ labelling threshold inside the sentence was read as a concentration limit and ten
115
+ labelling-only entries shipped as findings. One of them, Eucalyptus Globulus Leaf
116
+ Oil at 94 lists, was the fourth row of an earlier draft of the table above.
117
+
118
+ Entries are now classified from their own text, three ways rather than two. The
119
+ labelling sentence is removed; a maximum concentration or permitted product type
120
+ surviving that removal makes it a restriction on use. An entry in Annex II is a
121
+ prohibition and needs no conditions column. An entry whose restriction text was
122
+ never recovered is reported as neither, because promoting it invents a finding
123
+ and dropping it hides a date.
124
+
125
+ A separate defect from the same audit: the chemical name
126
+ `1-(2,6,6-trimethyl-2-cyclohexen-1-yl)-2-buten-1-one (16)` had its trailing
127
+ bracket split off as a synonym, producing a matchable substance named `16`. Any
128
+ label carrying a bare 16 would have matched it.
129
+
130
+ ## What it cannot tell you
131
+
132
+ **It cannot tell you whether your product complies.** Every restriction here
133
+ catches products that do not meet a new condition, and those conditions are
134
+ concentrations and product types. A printed ingredient list states no
135
+ concentrations. The tool can tell you a date applies to an ingredient you use. It
136
+ cannot tell you which side of it you are on. The rule text is printed with every
137
+ finding so you can check it yourself.
138
+
139
+ **Some conditions are not formulation choices at all.** The 1 January 2027
140
+ condition on Diethylamino Hydroxybenzoyl Hexyl Benzoate is a limit of 10 ppm on a
141
+ trace impurity in the filter you buy. Several entries turn on a peroxide value.
142
+ Those are answered by a supplier certificate, not by a recipe.
143
+
144
+ **Some of these rules are already in force.** Regulation 2023/1545's placing limb
145
+ passed on 31 July 2026 and Regulation 2024/996's on 1 November 2025. What remains
146
+ is the sell-through window. Seven of the 21 shipped entries are in that state and
147
+ are labelled as such on every run, because calling a rule already in force "not
148
+ yet applying" is simply false.
149
+
150
+ **It cannot see the products at risk under a labelling rule.** 50 substances carry
151
+ a dated duty to be named on the pack above a threshold. A list naming one is
152
+ meeting that duty. The list that omits it is the one at risk and is invisible
153
+ here. These are reported separately on every run and never counted.
154
+
155
+ **The percentage understates in one direction as well as overstating in the
156
+ other.** Annex III entry 379 as inserted by Regulation 2026/909 covers
157
+ "Aluminium-containing ingredients with the exception of those listed in entries
158
+ 34, 50, 189, 190 and 192 of Annex III...", with the same 1 January 2027 date.
159
+ There is no honest way to match a class with an exception list against a printed
160
+ name, so it is held back. Aluminium compounds are among the most common things in
161
+ antiperspirants and colour cosmetics, so the true number of products in this
162
+ corpus carrying a 1 January 2027 obligation is materially higher than 17.3 per
163
+ cent. Both sentences belong with the number.
164
+
165
+ **Three entries had no restriction text recoverable.** Several Annex III entries
166
+ print across multiple rows with the conditions on a parent row. Those three are
167
+ reported and never matched.
168
+
169
+ **One sentence was not interpreted.** A sentence mentioning a date and the Union
170
+ market in wording the builder does not recognise is recorded verbatim rather than
171
+ guessed at. `on-notice --unresolved` prints it.
172
+
173
+ **The register is a scan, not the law.** It covers Commission regulations amending
174
+ Regulation (EC) No 1223/2009 from 2013 onward, read from EUR-Lex: 52 listed, 52
175
+ read, 0 that could not be downloaded. 120 dated provisions found, 703 substance
176
+ links made, of which 21 are dated after the build and shipped. 9 provisions could
177
+ not be tied to named substances; every one has a date already in the past, so none
178
+ reaches the shipped register. National rules, rules outside the Cosmetics
179
+ Regulation, and anything adopted after the build date are outside it.
180
+
181
+ **A quiet answer is not a clean bill.** The tool prints what it could not see on
182
+ every run, including runs that find nothing.
183
+
184
+ ## How the register is built
185
+
186
+ `scripts/build_register.py` reads each regulation from EUR-Lex, parses the annex
187
+ tables, links footnote markers to the entries carrying them, and extracts the
188
+ dates. Four things about those documents broke a first attempt, all silently:
189
+
190
+ - Regulation 2023/1545 states its deadline as "may be placed on the Union market
191
+ until 31 July 2026", not as "shall not be". A parser looking only for the
192
+ negative form reports nothing for every substance in it.
193
+ - A footnote can put two dates and two restrictions in consecutive sentences. A
194
+ pattern crossing the sentence boundary pairs the first date with the second
195
+ restriction, and every sell-through date comes out wrong.
196
+ - EUR-Lex serves two different markups for footnotes and prints the same marker
197
+ three ways.
198
+ - Annex attribution reads from lead-in wording that takes several forms, so
199
+ entries were filed under the wrong annex while looking entirely correct.
200
+
201
+ One fetch returned HTTP 200 with an empty body, and the build reported it as a
202
+ regulation read with no deadlines in it, which is indistinguishable from the
203
+ truth. Responses are now checked for Official Journal markup before being
204
+ accepted, and a bad one is recorded as a failure rather than cached.
205
+
206
+ `docs/how-it-reads-the-source.md` records what these documents actually look like.
207
+
208
+ ## Install
209
+
210
+ Not published to the package index.
211
+
212
+ ```bash
213
+ git clone https://github.com/Waiga/on-notice
214
+ cd on-notice
215
+ pip install -e .
216
+ ```
217
+
218
+ Python 3.9 or later. No dependencies.
219
+
220
+ ## Licence
221
+
222
+ MIT.
@@ -0,0 +1,14 @@
1
+ """on-notice: which ingredients have an EU deadline that has not arrived yet.
2
+
3
+ The tool reads a cosmetic ingredient list and reports the ingredients on it that
4
+ are named in an EU regulation already adopted but not yet applying, and the date
5
+ each restriction starts. It never says a product is legal or illegal, compliant
6
+ or non-compliant. It reports a date and the rule that carries it.
7
+ """
8
+
9
+ __version__ = "0.1.0"
10
+
11
+ from .register import Register, Finding, load_register
12
+ from .ingredients import read_ingredients
13
+
14
+ __all__ = ["Register", "Finding", "load_register", "read_ingredients", "__version__"]
@@ -0,0 +1,6 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ sys.exit(main())