@readystack/green-claim-lint-empco 1.0.0
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.
- package/LICENSE.txt +16 -0
- package/README.md +54 -0
- package/cli.js +120 -0
- package/engine.js +124 -0
- package/license.js +64 -0
- package/package.json +42 -0
- package/rules.json +234 -0
- package/strings.json +10 -0
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Green Claim Lint — EU EmpCo 2026 — Licence
|
|
2
|
+
|
|
3
|
+
Free scope
|
|
4
|
+
Scan the page you have open and get every banned-claim hit with its Directive (EU) 2024/825 clause and a compliant rewrite. is free for any use, personal or commercial, for as long as you keep the
|
|
5
|
+
extension installed, and the online check at the product page is free without registration.
|
|
6
|
+
|
|
7
|
+
Paid scope
|
|
8
|
+
Scan the whole workspace in one command and export a dated evidence report (CSV + Markdown) you keep and hand to counsel or a marketplace. is the paid part. It asks for a licence key issued at purchase (one seat, one key;
|
|
9
|
+
7-day full refund, no questions). The key is checked against the payment provider and
|
|
10
|
+
cached locally for 30 days so it keeps working offline.
|
|
11
|
+
|
|
12
|
+
Redistribution
|
|
13
|
+
You may not resell, re-host or bundle this extension or its rule set. The rule set and the
|
|
14
|
+
engine are provided as-is; check results are advisory and do not constitute legal advice.
|
|
15
|
+
|
|
16
|
+
(c) ReadyStack — https://getreadystack.com
|
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Green Claim Lint - EU EmpCo 2026
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Reads your HTML and Markdown product copy and flags the environmental claims Directive (EU) 2024/825 turns into banned practices on 27 September 2026, with the clause and a compliant rewrite for each hit.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
npx @readystack/green-claim-lint-empco file
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Node 18+. The same 13 rules as the VS Code extension, from a terminal or CI.
|
|
14
|
+
|
|
15
|
+
## Free
|
|
16
|
+
|
|
17
|
+
- Open a product page in VS Code or paste it into the free web version and get every banned-claim hit with its Directive (EU) 2024/825 clause, the exact phrase, and a compliant rewrite - all 13 rules, no key, no limit on how many pages you open one at a time.
|
|
18
|
+
- `--rules` lists every rule
|
|
19
|
+
|
|
20
|
+
## With a licence ($29 once)
|
|
21
|
+
|
|
22
|
+
- Scan the entire workspace in one command and export a dated evidence report (CSV plus Markdown) that lists every page, hit, clause and rewrite - a file you keep and hand to counsel, a marketplace or an auditor.
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
@readystack/green-claim-lint-empco --dir ./templates --report html --out report.html
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
UCPD Art. 13(3): for widespread infringements Member States must provide fines of at least 4% of the trader's annual turnover in the Member States concerned, or at least EUR 2,000,000 where turnover information is unavailable.
|
|
29
|
+
|
|
30
|
+
## Use from an AI agent (MCP)
|
|
31
|
+
|
|
32
|
+
Claude Code · Cursor · Windsurf · any MCP client - add to your MCP config:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{ "mcpServers": { "green-claim-lint-empco": { "command": "npx", "args": ["-y", "@readystack/green-claim-lint-empco", "--mcp"] } } }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Tools: `check_text` and `check_file` (free) · `check_dir` (licence; the full sweep is free for 7 days). The agent gets every finding with the line number.
|
|
39
|
+
|
|
40
|
+
## Use in CI
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
- name: Green Claim Lint - EU EmpCo 2026
|
|
44
|
+
run: npx -y @readystack/green-claim-lint-empco --dir . --ci
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
(container: `docker run --rm -v "$PWD:/work" getreadystack/green-claim-lint-empco --dir /work --ci`)
|
|
48
|
+
|
|
49
|
+
Try the full run free for 7 days — no key needed. Then one licence, 7-day refund, no questions. Set `READYSTACK_LICENSE=<key>` or run `--license <key>` once.
|
|
50
|
+
|
|
51
|
+
[Get a licence](https://buy.polar.sh/polar_cl_2e1YKPUdH5MqiTNOPxNdoIn9RO4jK6SCn9fKb3VYDmc)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
<!-- green claim lint empco -->
|
package/cli.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// cli.js — the same rules as the VS Code extension, run from a terminal or a container. Generated by adapters.py.
|
|
3
|
+
'use strict';
|
|
4
|
+
const fs = require('fs'), path = require('path');
|
|
5
|
+
const RULES = require('./rules.json');
|
|
6
|
+
const S = require('./strings.json');
|
|
7
|
+
const lic = require('./license.js');
|
|
8
|
+
const ENGINE = require('./engine.js'); // s144 — scaffold products: the same engine the VS Code extension runs
|
|
9
|
+
const RULE_LIST = Array.isArray(ENGINE.RULES) ? ENGINE.RULES : (Array.isArray(RULES) ? RULES : []);
|
|
10
|
+
const RULE_N = ENGINE.RULE_COUNT || RULE_LIST.length;
|
|
11
|
+
function scan(text, file) {
|
|
12
|
+
const r = ENGINE.engine.check(String(text), { today: new Date().toISOString().slice(0, 10), path: file || '' });
|
|
13
|
+
return (r && r.findings || []).map((f) => ({ line: parseInt(f.line, 10) || 1, msg: String(f.msg || f.message || f.check || ''), fix: f.fix || null, sev: (String(f.sev || 'error').toLowerCase().startsWith('err') ? 'error' : 'warn') }));
|
|
14
|
+
}
|
|
15
|
+
function isText(p) { try { const b = fs.readFileSync(p); if (b.length > 2 * 1024 * 1024) return false; const s = b.subarray(0, 4096); for (const x of s) if (x === 0) return false; return true; } catch (e) { return false; } }
|
|
16
|
+
function walk(dir, exts, out) {
|
|
17
|
+
let ents = []; try { ents = fs.readdirSync(dir, { withFileTypes: true }); } catch (e) { return out; }
|
|
18
|
+
for (const e of ents) {
|
|
19
|
+
if (e.name === 'node_modules' || e.name === '.git' || e.name.startsWith('.')) continue;
|
|
20
|
+
const p = path.join(dir, e.name);
|
|
21
|
+
if (e.isDirectory()) walk(p, exts, out);
|
|
22
|
+
else if ((!exts.length || exts.includes(path.extname(e.name).toLowerCase())) && isText(p)) out.push(p);
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
function esc(s) { return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
|
|
27
|
+
function csvq(s) { return '"' + String(s == null ? '' : s).replace(/"/g, '""') + '"'; }
|
|
28
|
+
function render(rows, fmt) {
|
|
29
|
+
if (fmt === 'json') return JSON.stringify({ tool: S.name, rules: RULE_N, files: rows }, null, 1);
|
|
30
|
+
if (fmt === 'csv') { const o = ['file,line,severity,message,fix']; for (const r of rows) for (const h of r.hits) o.push([csvq(r.file), h.line, h.sev, csvq(h.msg), csvq(h.fix)].join(',')); return o.join('\n') + '\n'; }
|
|
31
|
+
if (fmt === 'html') {
|
|
32
|
+
const n = rows.reduce((a, r) => a + r.hits.length, 0);
|
|
33
|
+
let o = '<!doctype html><meta charset="utf-8"><title>' + esc(S.name) + ' report</title><style>body{font:14px system-ui;margin:24px}table{border-collapse:collapse}td,th{border:1px solid #ddd;padding:4px 8px;font-size:13px}code{font-family:ui-monospace,monospace}</style>';
|
|
34
|
+
o += '<h1>' + esc(S.name) + '</h1><p>' + rows.length + ' files · ' + n + ' findings · ' + RULE_N + ' rules</p><table><tr><th>file</th><th>line</th><th>sev</th><th>message</th><th>fix</th></tr>';
|
|
35
|
+
for (const r of rows) for (const h of r.hits) o += '<tr><td><code>' + esc(r.file) + '</code></td><td>' + h.line + '</td><td>' + esc(h.sev) + '</td><td>' + esc(h.msg) + '</td><td>' + esc(h.fix || '') + '</td></tr>';
|
|
36
|
+
return o + '</table>';
|
|
37
|
+
}
|
|
38
|
+
let o = ''; for (const r of rows) { o += r.file + '\n'; for (const h of r.hits) o += ' ' + String(h.line).padStart(5) + ' ' + h.sev.padEnd(5) + ' ' + h.msg + (h.fix ? '\n -> ' + h.fix : '') + '\n'; if (!r.hits.length) o += ' (no findings)\n'; }
|
|
39
|
+
return o;
|
|
40
|
+
}
|
|
41
|
+
function trialState() {
|
|
42
|
+
if (process.env.READYSTACK_NO_TRIAL) return { active: false };
|
|
43
|
+
try {
|
|
44
|
+
const p = path.join(path.dirname(lic.storePath()), S.bin + '.trial.json');
|
|
45
|
+
let t = null; try { t = JSON.parse(fs.readFileSync(p, 'utf8')); } catch (e) {}
|
|
46
|
+
if (!t || !t.until) { t = { until: new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString().slice(0, 10) }; fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, JSON.stringify(t)); }
|
|
47
|
+
return { active: new Date(t.until + 'T23:59:59Z').getTime() > Date.now(), until: t.until };
|
|
48
|
+
} catch (e) { return { active: false }; }
|
|
49
|
+
}
|
|
50
|
+
function mcpServe() {
|
|
51
|
+
// s144 — MCP server over stdio (newline-delimited JSON-RPC · no dependencies). Free: check_text · check_file. Licence (7-day trial): check_dir.
|
|
52
|
+
const rl = require('readline').createInterface({ input: process.stdin });
|
|
53
|
+
const send = (o) => process.stdout.write(JSON.stringify(o) + '\n');
|
|
54
|
+
const tools = [
|
|
55
|
+
{ name: 'check_text', description: S.name + ' - run all ' + RULE_N + ' checks on a text (free)', inputSchema: { type: 'object', properties: { text: { type: 'string', description: 'file contents' }, path: { type: 'string', description: 'optional file name for context' } }, required: ['text'] } },
|
|
56
|
+
{ name: 'check_file', description: S.name + ' - run all checks on one file by path (free)', inputSchema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } },
|
|
57
|
+
{ name: 'check_dir', description: S.name + ' - sweep a folder and return every finding (licence; the full run is free for 7 days)', inputSchema: { type: 'object', properties: { dir: { type: 'string' }, ext: { type: 'string', description: 'optional extension filter, e.g. .html' } }, required: ['dir'] } }
|
|
58
|
+
];
|
|
59
|
+
const result = (id, rows) => send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: render(rows, 'text') }], structuredContent: { tool: S.name, rules: RULE_N, files: rows } } });
|
|
60
|
+
const fail = (id, text) => send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text }], isError: true } });
|
|
61
|
+
rl.on('line', async (line) => {
|
|
62
|
+
let m; try { m = JSON.parse(line); } catch (e) { return; }
|
|
63
|
+
const id = m.id, method = m.method;
|
|
64
|
+
if (method === 'initialize') return send({ jsonrpc: '2.0', id, result: { protocolVersion: '2025-06-18', capabilities: { tools: {} }, serverInfo: { name: '@readystack/' + S.bin, version: '1.0.0' } } });
|
|
65
|
+
if (method === 'notifications/initialized' || method === 'ping') { if (id !== undefined) send({ jsonrpc: '2.0', id, result: {} }); return; }
|
|
66
|
+
if (method === 'tools/list') return send({ jsonrpc: '2.0', id, result: { tools } });
|
|
67
|
+
if (method === 'tools/call') {
|
|
68
|
+
const name = (m.params || {}).name, args = (m.params || {}).arguments || {};
|
|
69
|
+
try {
|
|
70
|
+
if (name === 'check_text') return result(id, [{ file: args.path || '(text)', hits: scan(String(args.text || ''), args.path || '') }]);
|
|
71
|
+
if (name === 'check_file') return result(id, [{ file: args.path, hits: scan(fs.readFileSync(args.path, 'utf8'), args.path) }]);
|
|
72
|
+
if (name === 'check_dir') {
|
|
73
|
+
const r = await lic.ensure();
|
|
74
|
+
if (!r.ok && !trialState().active) return fail(id, S.need_key + ' Get a licence ($' + S.price + ', once, 7-day refund): ' + lic.BUY_URL);
|
|
75
|
+
const files = walk(args.dir, args.ext ? [args.ext] : (S.exts || []), []);
|
|
76
|
+
return result(id, files.map((f) => { let t = ''; try { t = fs.readFileSync(f, 'utf8'); } catch (e) { return { file: f, hits: [], error: String(e.message) }; } return { file: f, hits: scan(t, f) }; }));
|
|
77
|
+
}
|
|
78
|
+
return send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'unknown tool ' + name } });
|
|
79
|
+
} catch (e) { return fail(id, String(e && e.message || e)); }
|
|
80
|
+
}
|
|
81
|
+
if (id !== undefined) send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'method not found: ' + method } });
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function help() {
|
|
85
|
+
return [S.name + ' - ' + S.subtitle, '', 'Usage: ' + S.bin + ' <file> [more files] check the files you name (free, every rule)',
|
|
86
|
+
' ' + S.bin + ' --dir <folder> [--ext .html] scan a whole folder (licence)',
|
|
87
|
+
' ' + S.bin + ' ... --report csv|json|html [--out file] export a report (licence)',
|
|
88
|
+
' ' + S.bin + ' ... --ci exit 1 when an error-level finding exists (licence)',
|
|
89
|
+
' ' + S.bin + ' --license <key> store your licence key (or set READYSTACK_LICENSE)',
|
|
90
|
+
' ' + S.bin + ' --rules list the ' + RULE_N + ' rules',
|
|
91
|
+
' ' + S.bin + ' --mcp run as an MCP server (stdio) for Claude Code / Cursor / Windsurf - free checks, folder sweep needs a licence', '',
|
|
92
|
+
'Free: ' + S.free, 'Licence ($' + S.price + ', once, 7-day refund): ' + S.paid, 'Get a licence: ' + lic.BUY_URL, ''].join('\n');
|
|
93
|
+
}
|
|
94
|
+
(async function main() {
|
|
95
|
+
try { const _feed = await lic.pullFeed(); if (_feed && Array.isArray(_feed.rules)) { if (Array.isArray(ENGINE.RULES)) for (const r of _feed.rules) ENGINE.RULES.push(r); } } catch (e) {} // ★s134 구독 피드 병합 (키 있는 손님만)
|
|
96
|
+
const a = process.argv.slice(2);
|
|
97
|
+
const get = (k) => { const i = a.indexOf(k); return i >= 0 ? a[i + 1] : null; };
|
|
98
|
+
if (a.includes('--mcp')) { mcpServe(); return; }
|
|
99
|
+
if (!a.length || a.includes('--help') || a.includes('-h')) { process.stdout.write(help()); return; }
|
|
100
|
+
if (a.includes('--rules')) { process.stdout.write((RULE_LIST.length ? RULE_LIST.map((r, i) => String(i + 1).padStart(3) + ' [' + (r.sev || 'warn') + '] ' + (r.message || r.msg || r.id || '')) : Array.from({ length: RULE_N }, (_, i) => String(i + 1).padStart(3) + ' [engine] check ' + (i + 1))).join('\n') + '\n'); return; }
|
|
101
|
+
if (a.includes('--license')) { const r = await lic.ensure(get('--license')); process.stdout.write(r.ok ? 'Licence stored: ' + lic.storePath() + '\n' : 'Licence not accepted (' + r.why + '). Get one: ' + lic.BUY_URL + '\n'); process.exit(r.ok ? 0 : 2); }
|
|
102
|
+
const dir = get('--dir'), fmt = get('--report'), out = get('--out'), ci = a.includes('--ci');
|
|
103
|
+
const exts = a.includes('--ext') ? [get('--ext')] : (S.exts || []);
|
|
104
|
+
const paid = !!(dir || fmt || ci);
|
|
105
|
+
if (paid) {
|
|
106
|
+
const r = await lic.ensure();
|
|
107
|
+
if (!r.ok) {
|
|
108
|
+
const t = trialState(); // s144 reverse trial: the full run is free for 7 days from the first paid use, then the key
|
|
109
|
+
if (t.active) process.stderr.write('Trial: the full run is free until ' + t.until + ' — after that $' + S.price + ' once (7-day refund). Get a licence: ' + lic.BUY_URL + '\n');
|
|
110
|
+
else { process.stderr.write(S.need_key + '\n set READYSTACK_LICENSE=<key> or ' + S.bin + ' --license <key>\n Get a licence ($' + S.price + ', once): ' + lic.BUY_URL + '\n'); process.exit(2); }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const files = dir ? walk(dir, exts, []) : a.filter((x, i) => !x.startsWith('--') && !['--dir', '--report', '--out', '--ext', '--license'].includes(a[i - 1]));
|
|
114
|
+
if (!files.length) { process.stderr.write('No files. ' + S.bin + ' --help\n'); process.exit(2); }
|
|
115
|
+
const rows = files.map((f) => { let t = ''; try { t = fs.readFileSync(f, 'utf8'); } catch (e) { return { file: f, hits: [], error: String(e.message) }; } return { file: f, hits: scan(t) }; });
|
|
116
|
+
const text = render(rows, fmt || 'text');
|
|
117
|
+
if (out) fs.writeFileSync(out, text); else process.stdout.write(text.endsWith('\n') ? text : text + '\n');
|
|
118
|
+
const errors = rows.reduce((n, r) => n + r.hits.filter((h) => h.sev === 'error').length, 0);
|
|
119
|
+
if (ci && errors) process.exit(1);
|
|
120
|
+
})().catch((e) => { process.stderr.write(String(e && e.stack || e) + '\n'); process.exit(3); });
|
package/engine.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Green Claim Lint - EU EmpCo 2026
|
|
2
|
+
// Directive (EU) 2024/825 ("empowering consumers for the green transition")
|
|
3
|
+
// amends UCPD 2005/29/EC + CRD 2011/83/EU. Member State measures apply from 2026-09-27.
|
|
4
|
+
// Same file runs in Node (VS Code extension) and in the browser (free web page).
|
|
5
|
+
|
|
6
|
+
(function (root) {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
var RULES = (typeof module !== 'undefined' && module.exports)
|
|
10
|
+
? require('./rules.json')
|
|
11
|
+
: root.GCL_RULES;
|
|
12
|
+
|
|
13
|
+
var APPLIES_FROM = RULES.applies_from; // 2026-09-27
|
|
14
|
+
|
|
15
|
+
function compile(rule) {
|
|
16
|
+
if (rule._c) return rule._c;
|
|
17
|
+
rule._c = {
|
|
18
|
+
any: (rule.any || []).map(function (p) { return new RegExp(p, 'i'); }),
|
|
19
|
+
not_line: (rule.not_line || []).map(function (p) { return new RegExp(p, 'i'); }),
|
|
20
|
+
doc_not_any: (rule.doc_not_any || []).map(function (p) { return new RegExp(p, 'i'); })
|
|
21
|
+
};
|
|
22
|
+
return rule._c;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Product copy arrives as HTML or Markdown. Strip tags, entities and code so a
|
|
26
|
+
// class name like <span class="eco-box"> is never read as a marketing claim.
|
|
27
|
+
function visibleText(line) {
|
|
28
|
+
return line
|
|
29
|
+
.replace(/<!--[\s\S]*?-->/g, ' ')
|
|
30
|
+
.replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gi, ' ')
|
|
31
|
+
.replace(/<[^>]*>/g, ' ')
|
|
32
|
+
.replace(/&[a-z]+;|&#\d+;/gi, ' ')
|
|
33
|
+
.replace(/`[^`]*`/g, ' ')
|
|
34
|
+
.replace(/\]\([^)]*\)/g, '] ')
|
|
35
|
+
.replace(/\s+/g, ' ')
|
|
36
|
+
.trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function daysBetween(a, b) {
|
|
40
|
+
return Math.round((Date.parse(b) - Date.parse(a)) / 86400000);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function check(text, opts) {
|
|
44
|
+
opts = opts || {};
|
|
45
|
+
var today = opts.today || new Date().toISOString().slice(0, 10);
|
|
46
|
+
var raw = String(text == null ? '' : text).split(/\r?\n/);
|
|
47
|
+
|
|
48
|
+
// Build the visible-text view once, skipping <script>/<style> blocks.
|
|
49
|
+
var lines = [];
|
|
50
|
+
var skip = null;
|
|
51
|
+
for (var i = 0; i < raw.length; i++) {
|
|
52
|
+
var l = raw[i];
|
|
53
|
+
if (skip) { if (skip.test(l)) skip = null; lines.push(''); continue; }
|
|
54
|
+
if (/<script\b/i.test(l) && !/<\/script>/i.test(l)) { skip = /<\/script>/i; lines.push(''); continue; }
|
|
55
|
+
if (/<style\b/i.test(l) && !/<\/style>/i.test(l)) { skip = /<\/style>/i; lines.push(''); continue; }
|
|
56
|
+
lines.push(visibleText(l));
|
|
57
|
+
}
|
|
58
|
+
var doc = lines.join('\n');
|
|
59
|
+
|
|
60
|
+
var findings = [];
|
|
61
|
+
for (var r = 0; r < RULES.rules.length; r++) {
|
|
62
|
+
var rule = RULES.rules[r];
|
|
63
|
+
var c = compile(rule);
|
|
64
|
+
|
|
65
|
+
// A page-level escape hatch: "repairable" is fine when the page also
|
|
66
|
+
// carries spare-parts / repair-manual information.
|
|
67
|
+
var docExempt = false;
|
|
68
|
+
for (var d = 0; d < c.doc_not_any.length; d++) {
|
|
69
|
+
if (c.doc_not_any[d].test(doc)) { docExempt = true; break; }
|
|
70
|
+
}
|
|
71
|
+
if (docExempt) continue;
|
|
72
|
+
|
|
73
|
+
for (var n = 0; n < lines.length; n++) {
|
|
74
|
+
var line = lines[n];
|
|
75
|
+
if (!line) continue;
|
|
76
|
+
|
|
77
|
+
var hit = null;
|
|
78
|
+
for (var a = 0; a < c.any.length; a++) {
|
|
79
|
+
var m = line.match(c.any[a]);
|
|
80
|
+
if (m) { hit = m[0]; break; }
|
|
81
|
+
}
|
|
82
|
+
if (!hit) continue;
|
|
83
|
+
|
|
84
|
+
var excused = false;
|
|
85
|
+
for (var x = 0; x < c.not_line.length; x++) {
|
|
86
|
+
if (c.not_line[x].test(line)) { excused = true; break; }
|
|
87
|
+
}
|
|
88
|
+
if (excused) continue;
|
|
89
|
+
|
|
90
|
+
findings.push({
|
|
91
|
+
check: rule.id,
|
|
92
|
+
sev: rule.sev,
|
|
93
|
+
msg: rule.msg + ' Found: "' + hit + '".',
|
|
94
|
+
line: n + 1,
|
|
95
|
+
clause: rule.clause,
|
|
96
|
+
phrase: hit,
|
|
97
|
+
fix: rule.fix,
|
|
98
|
+
text: line.length > 160 ? line.slice(0, 157) + '...' : line
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
findings.sort(function (p, q) { return p.line - q.line; });
|
|
104
|
+
|
|
105
|
+
var errors = findings.filter(function (f) { return f.sev === 'error'; }).length;
|
|
106
|
+
return {
|
|
107
|
+
findings: findings,
|
|
108
|
+
summary: {
|
|
109
|
+
law: RULES.law,
|
|
110
|
+
applies_from: APPLIES_FROM,
|
|
111
|
+
today: today,
|
|
112
|
+
days_to_empco: daysBetween(today, APPLIES_FROM),
|
|
113
|
+
rule_count: RULES.rules.length,
|
|
114
|
+
errors: errors,
|
|
115
|
+
warnings: findings.length - errors,
|
|
116
|
+
penalty: RULES.penalty
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
var api = { engine: { check: check }, RULES: RULES, RULE_COUNT: RULES.rules.length };
|
|
122
|
+
root.GCLENGINE = api;
|
|
123
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
124
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|
package/license.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// license.js — Polar licence check for the CLI / container. Generated by adapters.py; do not edit by hand.
|
|
2
|
+
'use strict';
|
|
3
|
+
const https = require('https'), fs = require('fs'), path = require('path'), os = require('os');
|
|
4
|
+
const ORG_ID = 'a5cdf664-d8e7-4f87-8895-056717aaba17';
|
|
5
|
+
const BENEFIT_ID = '33e42956-9fb0-4f7f-87d8-18b9002ef37b'; // s142 — this product's Polar benefit (s140 law: ask with benefit_id, or one key opens every product)
|
|
6
|
+
const BUY_URL = 'https://buy.polar.sh/polar_cl_2e1YKPUdH5MqiTNOPxNdoIn9RO4jK6SCn9fKb3VYDmc';
|
|
7
|
+
const SLUG = 'green-claim-lint-empco';
|
|
8
|
+
const GRACE_MS = 30 * 24 * 3600 * 1000; // after a successful check, 30 days work offline
|
|
9
|
+
const RECHECK_MS = 7 * 24 * 3600 * 1000; // re-ask Polar every 7 days (refunds / cancellations)
|
|
10
|
+
function storePath() { return path.join(process.env.READYSTACK_HOME || path.join(os.homedir(), '.config', 'readystack'), SLUG + '.json'); }
|
|
11
|
+
function load() { try { return JSON.parse(fs.readFileSync(storePath(), 'utf8')); } catch (e) { return {}; } }
|
|
12
|
+
function save(o) { try { fs.mkdirSync(path.dirname(storePath()), { recursive: true }); fs.writeFileSync(storePath(), JSON.stringify(o)); } catch (e) { /* read-only home: still works for this run */ } }
|
|
13
|
+
function validate(key) {
|
|
14
|
+
return new Promise(function (resolve) {
|
|
15
|
+
if (!ORG_ID) return resolve({ ok: false, offline: false });
|
|
16
|
+
const body = JSON.stringify(/^[0-9a-f-]{36}$/.test(BENEFIT_ID) ? { key: key, organization_id: ORG_ID, benefit_id: BENEFIT_ID } : { key: key, organization_id: ORG_ID });
|
|
17
|
+
const req = https.request({ hostname: 'api.polar.sh', path: '/v1/customer-portal/license-keys/validate', method: 'POST', timeout: 8000,
|
|
18
|
+
headers: { 'content-type': 'application/json', 'polar-version': '2026-04', 'content-length': Buffer.byteLength(body) } }, function (res) {
|
|
19
|
+
let buf = ''; res.on('data', function (d) { buf += d; });
|
|
20
|
+
res.on('end', function () {
|
|
21
|
+
if (res.statusCode !== 200) return resolve({ ok: false, offline: false });
|
|
22
|
+
try { const j = JSON.parse(buf); resolve({ ok: j && (j.status === 'granted' || j.valid === true || !!j.id), offline: false }); }
|
|
23
|
+
catch (e) { resolve({ ok: false, offline: false }); }
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
req.on('timeout', function () { req.destroy(); resolve({ ok: false, offline: true }); });
|
|
27
|
+
req.on('error', function () { resolve({ ok: false, offline: true }); });
|
|
28
|
+
req.write(body); req.end();
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
async function ensure(explicitKey) {
|
|
32
|
+
const st = load();
|
|
33
|
+
const key = explicitKey || process.env.READYSTACK_LICENSE || st.key;
|
|
34
|
+
if (!key) return { ok: false, why: 'no_key' };
|
|
35
|
+
const age = Date.now() - (st.okAt || 0);
|
|
36
|
+
if (!explicitKey && st.key === key && age < RECHECK_MS) return { ok: true, cached: true };
|
|
37
|
+
const r = await validate(String(key).trim());
|
|
38
|
+
if (r.ok) { save({ key: String(key).trim(), okAt: Date.now() }); return { ok: true }; }
|
|
39
|
+
if (r.offline && st.key === key && age < GRACE_MS) return { ok: true, offline: true };
|
|
40
|
+
return { ok: false, why: r.offline ? 'offline' : 'invalid' };
|
|
41
|
+
}
|
|
42
|
+
// ★s134 — 구독 규칙 피드(층3 "바뀌면 업데이트"): 키 있는 손님만 · 7일마다 · 오프라인은 캐시. 워커 GET /api/rules/<slug>?key=
|
|
43
|
+
const FEED_URL = 'https://getreadystack.com/api/rules/';
|
|
44
|
+
function pullFeed() {
|
|
45
|
+
const st = load(); const key = process.env.READYSTACK_LICENSE || st.key; const cached = st.feed || null;
|
|
46
|
+
if (!key) return Promise.resolve(cached);
|
|
47
|
+
if (cached && (Date.now() - (st.feedAt || 0)) < RECHECK_MS) return Promise.resolve(cached);
|
|
48
|
+
return new Promise(function (resolve) {
|
|
49
|
+
let req;
|
|
50
|
+
try {
|
|
51
|
+
req = https.get(FEED_URL + encodeURIComponent(SLUG) + '?key=' + encodeURIComponent(key), { timeout: 8000, headers: { 'user-agent': 'readystack-cli' } }, function (res) {
|
|
52
|
+
let buf = ''; res.on('data', function (d) { buf += d; });
|
|
53
|
+
res.on('end', function () {
|
|
54
|
+
if (res.statusCode !== 200) return resolve(cached);
|
|
55
|
+
try { const j = JSON.parse(buf); if (!j || !Array.isArray(j.rules)) return resolve(cached); save(Object.assign(load(), { feed: j, feedAt: Date.now() })); resolve(j); }
|
|
56
|
+
catch (e) { resolve(cached); }
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
} catch (e) { return resolve(cached); }
|
|
60
|
+
req.on('timeout', function () { req.destroy(); resolve(cached); });
|
|
61
|
+
req.on('error', function () { resolve(cached); });
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
module.exports = { ensure, BUY_URL, storePath, pullFeed };
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@readystack/green-claim-lint-empco",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Reads your HTML and Markdown product copy and flags the environmental claims Directive (EU) 2024/825 turns into banned practices on 27 September 2026, with the clause and a compliant rewrite for each ",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"greenwashing",
|
|
11
|
+
"empco",
|
|
12
|
+
"green claims",
|
|
13
|
+
"eu directive 2024/825",
|
|
14
|
+
"ecommerce",
|
|
15
|
+
"compliance",
|
|
16
|
+
"marketing copy",
|
|
17
|
+
"sustainability"
|
|
18
|
+
],
|
|
19
|
+
"homepage": "https://getreadystack.com",
|
|
20
|
+
"funding": "https://buy.polar.sh/polar_cl_2e1YKPUdH5MqiTNOPxNdoIn9RO4jK6SCn9fKb3VYDmc",
|
|
21
|
+
"bin": {
|
|
22
|
+
"green-claim-lint-empco": "cli.js"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"mcpName": "io.github.jmshinhwa/green-claim-lint-empco",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/jmshinhwa/readystack-themes.git",
|
|
31
|
+
"directory": "green-claim-lint-empco"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"cli.js",
|
|
35
|
+
"license.js",
|
|
36
|
+
"rules.json",
|
|
37
|
+
"strings.json",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE.txt",
|
|
40
|
+
"engine.js"
|
|
41
|
+
]
|
|
42
|
+
}
|
package/rules.json
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
{
|
|
2
|
+
"law": "Directive (EU) 2024/825 (EmpCo) amending UCPD 2005/29/EC and CRD 2011/83/EU",
|
|
3
|
+
"transposed_by": "2026-03-27",
|
|
4
|
+
"applies_from": "2026-09-27",
|
|
5
|
+
"penalty": "UCPD Art. 13(3): for widespread infringements, fines of at least 4% of annual turnover in the Member States concerned, or at least EUR 2,000,000 where turnover information is unavailable.",
|
|
6
|
+
"rules": [
|
|
7
|
+
{
|
|
8
|
+
"id": "generic-env-claim",
|
|
9
|
+
"sev": "error",
|
|
10
|
+
"clause": "UCPD Annex I, point 4(a) (inserted by Dir. (EU) 2024/825 Art. 2(2))",
|
|
11
|
+
"msg": "Generic environmental claim with no demonstrated excellent environmental performance.",
|
|
12
|
+
"fix": "Replace with the specific measured aspect, e.g. \"outer shell is 62% recycled polyester, GRS certificate SGS-GRS-2025-114\".",
|
|
13
|
+
"any": [
|
|
14
|
+
"\\beco-?friendly\\b",
|
|
15
|
+
"\\benvironmentally[ -]friendly\\b",
|
|
16
|
+
"\\benvironment[ -]friendly\\b",
|
|
17
|
+
"\\bplanet[ -]friendly\\b",
|
|
18
|
+
"\\bearth[ -]friendly\\b",
|
|
19
|
+
"\\bgreen (product|products|packaging|choice|shipping|delivery|energy)\\b",
|
|
20
|
+
"\\b(gentle|kind) (on|to) the (planet|environment)\\b",
|
|
21
|
+
"\\bnature'?s friend\\b",
|
|
22
|
+
"\\bsustainable\\b",
|
|
23
|
+
"\\bbiodegradable\\b",
|
|
24
|
+
"\\beco\\b",
|
|
25
|
+
"\\bconscious (choice|design|product|collection)\\b"
|
|
26
|
+
],
|
|
27
|
+
"not_line": [
|
|
28
|
+
"EU Ecolabel",
|
|
29
|
+
"Nordic Swan",
|
|
30
|
+
"Blue Angel",
|
|
31
|
+
"EMAS",
|
|
32
|
+
"ISO 1402[0-9]",
|
|
33
|
+
"EN 13432",
|
|
34
|
+
"OK compost"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "offset-neutrality",
|
|
39
|
+
"sev": "error",
|
|
40
|
+
"clause": "UCPD Annex I, point 4(c) (inserted by Dir. (EU) 2024/825 Art. 2(2))",
|
|
41
|
+
"msg": "Neutral / reduced / positive environmental impact claimed on the basis of emissions offsetting. Banned outright from 27 Sep 2026 - no evidence can cure it.",
|
|
42
|
+
"fix": "Delete the neutrality wording. State only emissions you actually cut, with the scope and the measurement method.",
|
|
43
|
+
"any": [
|
|
44
|
+
"\\b(carbon|co2|co₂|climate)[ -]?(neutral|positive|negative)\\b",
|
|
45
|
+
"\\bnet[ -]zero (product|shipping|delivery|order|checkout|item)\\b",
|
|
46
|
+
"\\bclimate[ -]compensated\\b",
|
|
47
|
+
"\\boffsett?(s|ing)? (your |the |all )?(co2|carbon|emissions)\\b",
|
|
48
|
+
"\\bcarbon[ -]offsets?\\b"
|
|
49
|
+
],
|
|
50
|
+
"not_line": []
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"id": "uncertified-label",
|
|
54
|
+
"sev": "error",
|
|
55
|
+
"clause": "UCPD Annex I, point 2(a) (inserted by Dir. (EU) 2024/825 Art. 2(2))",
|
|
56
|
+
"msg": "Sustainability label / score that is not based on a certification scheme and was not established by a public authority.",
|
|
57
|
+
"fix": "Name the certification scheme and the certificate ID, or remove the badge.",
|
|
58
|
+
"any": [
|
|
59
|
+
"\\b(eco|green|sustainability|climate|planet)[ -]?(score|label|badge|rating|seal|stamp|mark)\\b",
|
|
60
|
+
"\\bour own (eco|green|sustainability)\\b",
|
|
61
|
+
"\\bcertified (sustainable|green|eco)\\b"
|
|
62
|
+
],
|
|
63
|
+
"not_line": [
|
|
64
|
+
"EU Ecolabel",
|
|
65
|
+
"Nordic Swan",
|
|
66
|
+
"Blue Angel",
|
|
67
|
+
"EMAS",
|
|
68
|
+
"ISO 1402[0-9]",
|
|
69
|
+
"\\bFSC\\b",
|
|
70
|
+
"\\bGRS\\b",
|
|
71
|
+
"\\bGOTS\\b",
|
|
72
|
+
"Cradle to Cradle",
|
|
73
|
+
"Energy Star",
|
|
74
|
+
"EU Energy Label"
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"id": "future-pledge",
|
|
79
|
+
"sev": "error",
|
|
80
|
+
"clause": "UCPD Art. 6(2)(d) (inserted by Dir. (EU) 2024/825 Art. 2(1))",
|
|
81
|
+
"msg": "Claim about future environmental performance without clear, objective, publicly available and verifiable commitments, a detailed implementation plan and independent third-party monitoring.",
|
|
82
|
+
"fix": "Link the published plan with interim targets and name the independent expert that verifies it, or drop the pledge from product copy.",
|
|
83
|
+
"any": [
|
|
84
|
+
"\\b(net[ -]zero|carbon[ -]neutral|climate[ -]neutral|plastic[ -]free|100% (renewable|circular|recycled|sustainable))\\b[^.]{0,50}\\bby 20\\d\\d\\b",
|
|
85
|
+
"\\bby 20\\d\\d\\b[^.]{0,50}\\b(net[ -]zero|carbon[ -]neutral|climate[ -]neutral)\\b",
|
|
86
|
+
"\\bwe (will|aim to|pledge to|commit to|are on track to) be(come)?\\b[^.]{0,40}\\b(neutral|zero|sustainable|circular)\\b"
|
|
87
|
+
],
|
|
88
|
+
"not_line": []
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"id": "whole-vs-part",
|
|
92
|
+
"sev": "error",
|
|
93
|
+
"clause": "UCPD Annex I, point 4(b) (inserted by Dir. (EU) 2024/825 Art. 2(2))",
|
|
94
|
+
"msg": "Environmental claim about the whole product, range or business when it concerns only one aspect of it.",
|
|
95
|
+
"fix": "Scope the claim to the exact component or stage it was measured on.",
|
|
96
|
+
"any": [
|
|
97
|
+
"\\b(all (of )?our products|our (entire|whole) (range|catalogue|catalog|collection|line)|everything we (make|sell)|our whole (company|business)|the entire company)\\b[^.]{0,70}\\b(sustainable|green|eco|carbon|climate|recycled|circular|responsible)\\b",
|
|
98
|
+
"\\b(sustainable|green|eco[ -]?friendly|circular|climate[ -]conscious)\\b[^.]{0,40}\\b(company|brand|business)\\b"
|
|
99
|
+
],
|
|
100
|
+
"not_line": []
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"id": "legal-as-feature",
|
|
104
|
+
"sev": "warn",
|
|
105
|
+
"clause": "UCPD Annex I, point 10 (scope extended by Dir. (EU) 2024/825)",
|
|
106
|
+
"msg": "A right or requirement that the law already imposes is presented as a distinctive feature of your offer.",
|
|
107
|
+
"fix": "State the statutory right plainly, without framing it as a bonus or a reason to choose you.",
|
|
108
|
+
"any": [
|
|
109
|
+
"\\bfree (2|two)[ -]year (warranty|guarantee)\\b",
|
|
110
|
+
"\\b(2|two)[ -]year (warranty|guarantee)\\b[^.]{0,40}\\b(free|included at no|bonus|extra|unlike|only from us)\\b",
|
|
111
|
+
"\\b(14|fourteen)[ -]day (right of withdrawal|returns?|money[ -]back)\\b[^.]{0,40}\\b(free|bonus|extra|our promise|unlike|generous)\\b",
|
|
112
|
+
"\\b(rohs|reach|ce[ -]marked|gdpr)\\b[^.]{0,45}\\b(unlike|unique|exclusive|why choose|our advantage|sets us apart)\\b"
|
|
113
|
+
],
|
|
114
|
+
"not_line": []
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"id": "durability-unproven",
|
|
118
|
+
"sev": "error",
|
|
119
|
+
"clause": "UCPD Annex I, point 23(d) (inserted by Dir. (EU) 2024/825 Art. 2(2))",
|
|
120
|
+
"msg": "Durability claimed in usage time or intensity with no test or evidence on the page.",
|
|
121
|
+
"fix": "Cite the durability test standard and the result, or delete the claim.",
|
|
122
|
+
"any": [
|
|
123
|
+
"\\blasts? (a )?lifetime\\b",
|
|
124
|
+
"\\blifetime (durability|guarantee)\\b",
|
|
125
|
+
"\\blasts? forever\\b",
|
|
126
|
+
"\\bbuilt to last forever\\b",
|
|
127
|
+
"\\bunbreakable\\b",
|
|
128
|
+
"\\blasts? (up to )?\\d+ years\\b",
|
|
129
|
+
"\\b\\d+[ -]year (lifespan|life span|service life)\\b"
|
|
130
|
+
],
|
|
131
|
+
"not_line": [
|
|
132
|
+
"test(ed|ing|s)? (to|under|report|result)",
|
|
133
|
+
"EN ISO",
|
|
134
|
+
"IEC \\d",
|
|
135
|
+
"cycle test",
|
|
136
|
+
"substantiat",
|
|
137
|
+
"test report"
|
|
138
|
+
]
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"id": "repairability-claim",
|
|
142
|
+
"sev": "warn",
|
|
143
|
+
"clause": "UCPD Annex I, point 23(e) (inserted by Dir. (EU) 2024/825 Art. 2(2))",
|
|
144
|
+
"msg": "Repairability claimed but the page carries no spare-parts, repair-manual or repairability-index information.",
|
|
145
|
+
"fix": "Add the spare-parts availability period and a link to the repair manual on the same page.",
|
|
146
|
+
"any": [
|
|
147
|
+
"\\b(fully |easily |easy to )?repairable\\b",
|
|
148
|
+
"\\bdesigned (for|to be) repair",
|
|
149
|
+
"\\beasy to (repair|fix)\\b"
|
|
150
|
+
],
|
|
151
|
+
"not_line": [],
|
|
152
|
+
"doc_not_any": [
|
|
153
|
+
"spare part",
|
|
154
|
+
"repair manual",
|
|
155
|
+
"repairability (index|score)",
|
|
156
|
+
"repair score",
|
|
157
|
+
"repair guide"
|
|
158
|
+
]
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"id": "premature-replacement",
|
|
162
|
+
"sev": "warn",
|
|
163
|
+
"clause": "UCPD Annex I, point 23(f) (inserted by Dir. (EU) 2024/825 Art. 2(2))",
|
|
164
|
+
"msg": "Copy prompts the consumer to replace or replenish a consumable earlier than technically necessary.",
|
|
165
|
+
"fix": "Give the condition that actually calls for replacement, not a fixed calendar interval.",
|
|
166
|
+
"any": [
|
|
167
|
+
"\\breplace (it |them |your [a-z]+ )?(every|after) \\d+ (days|weeks|months)\\b",
|
|
168
|
+
"\\bfor best results,? replace\\b",
|
|
169
|
+
"\\breplace (monthly|weekly|each month|every month)\\b"
|
|
170
|
+
],
|
|
171
|
+
"not_line": []
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"id": "update-necessity",
|
|
175
|
+
"sev": "warn",
|
|
176
|
+
"clause": "UCPD Annex I, points 23(g) and 23(h) (inserted by Dir. (EU) 2024/825 Art. 2(2))",
|
|
177
|
+
"msg": "A software update is presented as necessary. If it only enhances functionality this is banned, and any update that degrades the goods must be disclosed.",
|
|
178
|
+
"fix": "Say what the update changes and whether it affects existing functions.",
|
|
179
|
+
"any": [
|
|
180
|
+
"\\b(required|mandatory|essential|critical|must[ -]install) (firmware |software |app )?update\\b",
|
|
181
|
+
"\\byou must update\\b",
|
|
182
|
+
"\\bupdate (is )?required to keep (using|your)\\b"
|
|
183
|
+
],
|
|
184
|
+
"not_line": [
|
|
185
|
+
"security (fix|patch|update)",
|
|
186
|
+
"vulnerabilit"
|
|
187
|
+
]
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"id": "irrelevant-benefit",
|
|
191
|
+
"sev": "warn",
|
|
192
|
+
"clause": "UCPD Art. 6(2)(c) and Annex I, point 4 (Dir. (EU) 2024/825)",
|
|
193
|
+
"msg": "A benefit is advertised that is irrelevant or that the law already requires of every product in this category.",
|
|
194
|
+
"fix": "Remove it, or state it as background information rather than an advantage.",
|
|
195
|
+
"any": [
|
|
196
|
+
"\\bcfc[ -]free\\b",
|
|
197
|
+
"\\blead[ -]free (paint|solder|crystal)\\b",
|
|
198
|
+
"\\b(gluten|fat|sugar)[ -]free (water|salt)\\b",
|
|
199
|
+
"\\bnon[ -]gmo (salt|water)\\b",
|
|
200
|
+
"\\bphosphate[ -]free\\b",
|
|
201
|
+
"\\bmicroplastic[ -]free (glass|metal|steel)\\b"
|
|
202
|
+
],
|
|
203
|
+
"not_line": []
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"id": "unquantified-recycled",
|
|
207
|
+
"sev": "warn",
|
|
208
|
+
"clause": "UCPD Art. 6(1)(b) as amended by Dir. (EU) 2024/825",
|
|
209
|
+
"msg": "Recycled content asserted without the share and the part it applies to.",
|
|
210
|
+
"fix": "Write the percentage and the component, e.g. \"lining: 40% post-consumer recycled polyamide\".",
|
|
211
|
+
"any": [
|
|
212
|
+
"\\bmade (from|with|of) recycled (materials?|content|plastic|fabric)\\b",
|
|
213
|
+
"\\bcontains recycled (materials?|content)\\b",
|
|
214
|
+
"\\bpartly recycled\\b",
|
|
215
|
+
"\\brecycled materials\\b"
|
|
216
|
+
],
|
|
217
|
+
"not_line": [
|
|
218
|
+
"\\d{1,3} ?% (recycled|post[ -]consumer)"
|
|
219
|
+
]
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
"id": "vague-maximum",
|
|
223
|
+
"sev": "warn",
|
|
224
|
+
"clause": "UCPD Art. 6(1)(b) as amended by Dir. (EU) 2024/825",
|
|
225
|
+
"msg": "An \"up to\" figure states the best case only and hides the actual share the buyer gets.",
|
|
226
|
+
"fix": "State the typical or minimum value, or give the range.",
|
|
227
|
+
"any": [
|
|
228
|
+
"\\bup to \\d+ ?%",
|
|
229
|
+
"\\bup to \\d+ (times|x) (less|lower|fewer|more)\\b"
|
|
230
|
+
],
|
|
231
|
+
"not_line": []
|
|
232
|
+
}
|
|
233
|
+
]
|
|
234
|
+
}
|
package/strings.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Green Claim Lint - EU EmpCo 2026",
|
|
3
|
+
"subtitle": "Reads your HTML and Markdown product copy and flags the environmental claims Directive (EU) 2024/825 turns into banned practices on 27 September 2026, with the clause and a compliant rewrite for each hit.",
|
|
4
|
+
"bin": "green-claim-lint-empco",
|
|
5
|
+
"price": 29,
|
|
6
|
+
"free": "Open a product page in VS Code or paste it into the free web version and get every banned-claim hit with its Directive (EU) 2024/825 clause, the exact phrase, and a compliant rewrite - all 13 rules, no key, no limit on how many pages you open one at a time.",
|
|
7
|
+
"paid": "Scan the entire workspace in one command and export a dated evidence report (CSV plus Markdown) that lists every page, hit, clause and rewrite - a file you keep and hand to counsel, a marketplace or an auditor.",
|
|
8
|
+
"need_key": "This option needs a licence (Green Claim Lint - EU EmpCo 2026).",
|
|
9
|
+
"exts": []
|
|
10
|
+
}
|