diff --git a/ai-act-compass.jsx b/ai-act-compass.jsx
index 88001ac..ca1cb74 100644
--- a/ai-act-compass.jsx
+++ b/ai-act-compass.jsx
@@ -604,6 +604,7 @@ const CHECKLIST = {
{ ref: 'art. 50(3)', txt: _('Inform subjects of biometric categorisation or emotion recognition', 'Information du sujet de catégorisation biométrique ou reconnaissance émotionnelle') },
{ ref: 'art. 50(4)', txt: _('"Deepfake" label on manipulated image/audio/video content', 'Label « deepfake » sur contenus image/audio/vidéo manipulés') },
{ ref: 'art. 50(4) §2', txt: _('Synthetic public-interest text labelling (except human-edited)', 'Étiquetage du texte synthétique d\'intérêt public (sauf édition humaine)') },
+ { ref: 'art. 50(5)', txt: _('Transparency information must be clear, distinguishable, and provided at the latest at first interaction/exposure; where applicable, comply with accessibility requirements under Directive 2019/882/EU (European Accessibility Act)', 'Information de transparence : claire, distinguable, fournie au plus tard à la 1ʳᵉ interaction/exposition ; lorsque applicable, conforme aux exigences d\'accessibilité de la directive 2019/882/UE (European Accessibility Act)') },
]},
{ pilier: _('AI literacy', 'AI literacy'), items: [
{ ref: 'art. 4', txt: _('AI literacy programme applicable since 2025-02-02', 'Programme AI literacy applicable depuis 2025-02-02') },
@@ -2172,8 +2173,8 @@ export default function App() {
const [lang, setLang] = useState('en'); // EN by default
const [step, setStep] = useState(0);
const [answers, setAnswers] = useState({
- role: null, nature: null, prohibitions: null, prohibitionCarveOuts: {}, annexI: null,
- annexIII: [], annexIII5Subitems: [], exceptions: null, profiling: false, art50: [], gpaiSystemic: null,
+ role: null, nature: null, prohibitions: null, prohibitionCarveOuts: {}, annexICoverage: null, annexI3rdPartyCA: null,
+ annexIII: [], annexIII5Subitems: [], exceptions: null, profiling: false, art50: [], art50TextHumanEdit: null, gpaiSystemic: null,
deployerKind: null, substantialModification: null,
});
@@ -2196,8 +2197,8 @@ export default function App() {
const restart = () => {
setAnswers({
- role: null, nature: null, prohibitions: null, prohibitionCarveOuts: {}, annexI: null,
- annexIII: [], annexIII5Subitems: [], exceptions: null, profiling: false, art50: [], gpaiSystemic: null,
+ role: null, nature: null, prohibitions: null, prohibitionCarveOuts: {}, annexICoverage: null, annexI3rdPartyCA: null,
+ annexIII: [], annexIII5Subitems: [], exceptions: null, profiling: false, art50: [], art50TextHumanEdit: null, gpaiSystemic: null,
deployerKind: null, substantialModification: null,
});
setStep(0);
@@ -2412,22 +2413,56 @@ export default function App() {
stepNum={4} totalSteps={TOTAL_STEPS}
title={t(UI.q4Title, lang)}
subtitle={t(UI.q4Sub, lang)}
- canNext={answers.annexI !== null}
+ canNext={answers.annexICoverage !== null && answers.annexI3rdPartyCA !== null}
onNext={next} onBack={back}
>
-
-
setAnswers({ ...answers, annexI: 'oui' })}
- title={t(UI.yes, lang)}
- sub={lang === 'en' ? 'art. 6(1) + Annex I' : 'art. 6(1) + Annexe I'}
- desc={t(UI.q4YesDesc, lang)}
- />
- setAnswers({ ...answers, annexI: 'non' })}
- title={t(UI.no, lang)} desc={t(UI.q4NoDesc, lang)}
- />
+
+
+
+ {lang === 'en' ? '1. Annex I coverage' : '1. Couverture Annexe I'}
+
+
setAnswers({ ...answers, annexICoverage: 'oui', annexI3rdPartyCA: null })}
+ title={t(UI.yes, lang)}
+ sub={lang === 'en' ? 'Annex I harmonisation legislation' : 'Législation d\'harmonisation Annexe I'}
+ desc={lang === 'en'
+ ? 'My system is a safety component of (or itself) a product covered by an Annex I sectoral regulation (machinery, medical devices, toys, motor vehicles, lifts, radio equipment, …).'
+ : 'Mon système est un composant de sécurité (ou est lui-même) un produit couvert par une réglementation sectorielle Annexe I (machines, dispositifs médicaux, jouets, automobiles, ascenseurs, équipements radio, …).'}
+ />
+ setAnswers({ ...answers, annexICoverage: 'non', annexI3rdPartyCA: 'non' })}
+ title={t(UI.no, lang)}
+ desc={lang === 'en' ? 'My system does not fall within Annex I scope.' : 'Mon système n\'entre pas dans le champ Annexe I.'}
+ />
+
+
+ {answers.annexICoverage === 'oui' && (
+
+
+ {lang === 'en' ? '2. Third-party conformity assessment' : '2. Évaluation de conformité par tiers'}
+
+
+ {lang === 'en'
+ ? 'art. 6(1) is cumulative: HAUT_RISQUE_ANNEXE_I requires BOTH Annex I coverage AND a third-party CA obligation under that sectoral regime.'
+ : 'L\'art. 6(1) est cumulatif : HAUT_RISQUE_ANNEXE_I exige la couverture Annexe I ET une obligation d\'évaluation de conformité par tiers sous ce régime sectoriel.'}
+
+
setAnswers({ ...answers, annexI3rdPartyCA: 'oui' })}
+ title={t(UI.yes, lang)}
+ sub="art. 6(1)"
+ desc={lang === 'en' ? 'The sectoral regulation requires third-party CA for this product.' : 'La réglementation sectorielle exige une évaluation de conformité par tiers pour ce produit.'}
+ />
+ setAnswers({ ...answers, annexI3rdPartyCA: 'non' })}
+ title={t(UI.no, lang)}
+ desc={lang === 'en' ? 'Self-assessment route under the sectoral regulation.' : 'Voie d\'auto-évaluation sous la réglementation sectorielle.'}
+ />
+
+ )}
)}
@@ -2590,13 +2625,43 @@ export default function App() {
onClick={() => {
const cur = answers.art50;
const upd = sel ? cur.filter(x => x !== tr.id) : [...cur, tr.id];
- setAnswers({ ...answers, art50: upd });
+ setAnswers({
+ ...answers,
+ art50: upd,
+ art50TextHumanEdit: upd.includes('genai_text') ? answers.art50TextHumanEdit : null,
+ });
}}
title={t(tr.label, lang)} sub={tr.ref} desc={t(tr.desc, lang)}
/>
);
})}
+
+ {(answers.art50 || []).includes('genai_text') && (
+
+
+ {lang === 'en' ? 'Human review carve-out (art. 50(4) §2)' : 'Exception revue humaine (art. 50(4) §2)'}
+
+
+ {lang === 'en'
+ ? 'art. 50(4) second subparagraph: the labelling obligation does NOT apply where the AI-generated text has undergone human review or editorial control and a natural/legal person holds editorial responsibility.'
+ : 'art. 50(4) deuxième alinéa : l\'obligation d\'étiquetage NE s\'applique PAS lorsque le texte généré a fait l\'objet d\'une revue ou d\'un contrôle éditorial humain et qu\'une personne physique/morale en assume la responsabilité éditoriale.'}
+
+
setAnswers({ ...answers, art50TextHumanEdit: 'oui' })}
+ title={lang === 'en' ? 'Yes — under editorial responsibility' : 'Oui — sous responsabilité éditoriale'}
+ sub="art. 50(4) §2"
+ desc={lang === 'en' ? 'Transparency obligation does not apply for this text channel.' : 'L\'obligation de transparence ne s\'applique pas pour ce canal de texte.'}
+ />
+ setAnswers({ ...answers, art50TextHumanEdit: 'non' })}
+ title={lang === 'en' ? 'No — fully automated publication' : 'Non — publication entièrement automatisée'}
+ desc={lang === 'en' ? 'Standard art. 50(4) §2 labelling applies.' : 'L\'étiquetage standard art. 50(4) §2 s\'applique.'}
+ />
+
+ )}
)}
diff --git a/src/lib/classify.js b/src/lib/classify.js
index 2437858..bc41f9e 100644
--- a/src/lib/classify.js
+++ b/src/lib/classify.js
@@ -325,10 +325,10 @@ export function computeCategory(answers, lang) {
const flipsViaArt25 = isOnGPAI && answers.substantialModification === 'oui';
const isGPAI_RS = (isGPAIProvider || flipsViaArt25) && answers.gpaiSystemic === 'oui';
- if (answers.annexI === 'oui') {
+ if (answers.annexICoverage === 'oui' && answers.annexI3rdPartyCA === 'oui') {
justifications.push({
ref: lang === 'en' ? 'art. 6(1) + Annex I' : 'art. 6(1) + Annexe I',
- label: lang === 'en' ? 'Safety component of a harmonised product' : 'Composant de sécurité de produit harmonisé',
+ label: lang === 'en' ? 'Safety component of a harmonised product subject to third-party conformity assessment' : 'Composant de sécurité de produit harmonisé soumis à évaluation de conformité par tiers',
});
categories.push('HAUT_RISQUE_ANNEXE_I');
}
@@ -369,11 +369,22 @@ export function computeCategory(answers, lang) {
}
const triggers = answers.art50 || [];
- if (triggers.length > 0) {
- triggers.forEach(id => {
+ const textHumanEdit = answers.art50TextHumanEdit === 'oui';
+ const effectiveTriggers = textHumanEdit ? triggers.filter(id => id !== 'genai_text') : triggers;
+ triggers.forEach(id => {
+ if (id === 'genai_text' && textHumanEdit) {
+ justifications.push({
+ ref: 'art. 50(4) §2 carve-out',
+ label: lang === 'en'
+ ? 'AI-generated public-interest text is under human review / editorial control — art. 50(4) §2 transparency obligation does not apply (counsel verification required).'
+ : 'Texte généré d\'intérêt public sous revue / édition humaine — l\'obligation de transparence art. 50(4) §2 ne s\'applique pas (vérification juridique requise).',
+ });
+ } else {
const tr = ART50_TRIGGERS.find(x => x.id === id);
if (tr) justifications.push({ ref: tr.ref, label: t(tr.label, lang) });
- });
+ }
+ });
+ if (effectiveTriggers.length > 0) {
categories.push('RISQUE_LIMITE');
}
diff --git a/src/lib/classify.test.js b/src/lib/classify.test.js
index 890f739..867235f 100644
--- a/src/lib/classify.test.js
+++ b/src/lib/classify.test.js
@@ -53,7 +53,8 @@ describe('art. 5 — prohibited practices', () => {
prohibitions: ['a'],
annexIII: [3, 4], // education + employment
art50: ['interaction'], // chatbot trigger
- annexI: 'oui',
+ annexICoverage: 'oui',
+ annexI3rdPartyCA: 'oui',
nature: 'gpai',
gpaiSystemic: 'oui',
}, 'en');
@@ -66,12 +67,36 @@ describe('art. 5 — prohibited practices', () => {
});
});
-describe('art. 6(1) — Annex I product safety pathway', () => {
- it('returns HAUT_RISQUE_ANNEXE_I when annexI === "oui"', () => {
- const result = computeCategory({ annexI: 'oui' }, 'en');
+describe('art. 6(1) — Annex I product safety pathway (cumulative test)', () => {
+ it('returns HAUT_RISQUE_ANNEXE_I when BOTH annexICoverage AND annexI3rdPartyCA are "oui"', () => {
+ const result = computeCategory({
+ annexICoverage: 'oui',
+ annexI3rdPartyCA: 'oui',
+ }, 'en');
expect(result.primary).toBe('HAUT_RISQUE_ANNEXE_I');
expect(result.justifications.some(j => /Annex I|Annexe I/.test(j.ref))).toBe(true);
});
+
+ it('does NOT trigger HAUT_RISQUE_ANNEXE_I when only coverage is "oui" (no 3rd-party CA)', () => {
+ const result = computeCategory({
+ annexICoverage: 'oui',
+ annexI3rdPartyCA: 'non',
+ }, 'en');
+ expect(result.primary).not.toBe('HAUT_RISQUE_ANNEXE_I');
+ });
+
+ it('does NOT trigger HAUT_RISQUE_ANNEXE_I when only 3rd-party CA is "oui" (no Annex I coverage)', () => {
+ const result = computeCategory({
+ annexICoverage: 'non',
+ annexI3rdPartyCA: 'oui',
+ }, 'en');
+ expect(result.primary).not.toBe('HAUT_RISQUE_ANNEXE_I');
+ });
+
+ it('does NOT trigger HAUT_RISQUE_ANNEXE_I when neither is set (defaults to null)', () => {
+ const result = computeCategory({}, 'en');
+ expect(result.primary).not.toBe('HAUT_RISQUE_ANNEXE_I');
+ });
});
describe('art. 6(2) — Annex III pathway (no exception)', () => {
@@ -162,7 +187,8 @@ describe('art. 51-55 + art. 25 — GPAI', () => {
describe('priority ordering across multiple triggers', () => {
it('orders categories: Annex I > Annex III > GPAI_RS > GPAI > RISQUE_LIMITE', () => {
const result = computeCategory({
- annexI: 'oui',
+ annexICoverage: 'oui',
+ annexI3rdPartyCA: 'oui',
annexIII: [3],
art50: ['interaction'],
nature: 'gpai',
@@ -208,7 +234,8 @@ describe('i18n parity', () => {
it('produces identical primary, secondary, and justification refs across en and fr (language-independent refs only)', () => {
const answers = {
prohibitions: [],
- annexI: 'non',
+ annexICoverage: 'non',
+ annexI3rdPartyCA: 'non',
annexIII: [],
art50: ['interaction', 'genai_media'],
nature: 'gpai',
@@ -524,3 +551,54 @@ describe('art. 25 — substantial-modification provider flip', () => {
expect(result.justifications.some(j => j.ref === 'art. 25')).toBe(false);
});
});
+
+describe('art. 50(4) §2 — human-edit exemption for AI-generated public-interest text', () => {
+ it('still triggers RISQUE_LIMITE for genai_text when no human-edit claim is made', () => {
+ const result = computeCategory({ art50: ['genai_text'] }, 'en');
+ expect(result.primary).toBe('RISQUE_LIMITE');
+ expect(result.justifications.some(j => j.ref === 'art. 50(4) §2')).toBe(true);
+ });
+
+ it('removes RISQUE_LIMITE contribution of genai_text when art50TextHumanEdit === "oui"', () => {
+ const result = computeCategory({
+ art50: ['genai_text'],
+ art50TextHumanEdit: 'oui',
+ }, 'en');
+ expect(result.primary).not.toBe('RISQUE_LIMITE');
+ expect(result.justifications.some(j => j.ref === 'art. 50(4) §2 carve-out')).toBe(true);
+ expect(result.justifications.some(j => j.ref === 'art. 50(4) §2')).toBe(false);
+ });
+
+ it('keeps other art50 triggers active when only genai_text is carved out', () => {
+ const result = computeCategory({
+ art50: ['genai_text', 'interaction'],
+ art50TextHumanEdit: 'oui',
+ }, 'en');
+ expect(result.primary).toBe('RISQUE_LIMITE');
+ // chatbot trigger still contributes
+ expect(result.justifications.some(j => j.ref === 'art. 50(1)')).toBe(true);
+ // genai_text is carved out
+ expect(result.justifications.some(j => j.ref === 'art. 50(4) §2')).toBe(false);
+ expect(result.justifications.some(j => j.ref === 'art. 50(4) §2 carve-out')).toBe(true);
+ });
+
+ it('emits a French label for the carve-out when lang === "fr"', () => {
+ const result = computeCategory({
+ art50: ['genai_text'],
+ art50TextHumanEdit: 'oui',
+ }, 'fr');
+ const carveOut = result.justifications.find(j => j.ref === 'art. 50(4) §2 carve-out');
+ expect(carveOut).toBeDefined();
+ expect(carveOut.label).toMatch(/édition humaine|revue éditoriale/i);
+ });
+
+ it('is a no-op when art50TextHumanEdit is set but genai_text is not selected', () => {
+ const result = computeCategory({
+ art50: ['interaction'],
+ art50TextHumanEdit: 'oui',
+ }, 'en');
+ expect(result.primary).toBe('RISQUE_LIMITE');
+ expect(result.justifications.some(j => j.ref === 'art. 50(4) §2 carve-out')).toBe(false);
+ expect(result.justifications.some(j => j.ref === 'art. 50(1)')).toBe(true);
+ });
+});