@euconform/core 1.0.1 → 1.2.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +6 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/inference/ollama-client.d.ts +1 -0
- package/dist/inference/ollama-client.d.ts.map +1 -1
- package/dist/inference/ollama-client.js +15 -5
- package/dist/legal-checks/annex-iv-report.d.ts +3 -0
- package/dist/legal-checks/annex-iv-report.d.ts.map +1 -1
- package/dist/legal-checks/annex-iv-report.js +1 -0
- package/dist/legal-checks/deadline-timeline.d.ts +57 -0
- package/dist/legal-checks/deadline-timeline.d.ts.map +1 -0
- package/dist/legal-checks/deadline-timeline.js +265 -0
- package/dist/legal-checks/gap-analysis.d.ts +48 -0
- package/dist/legal-checks/gap-analysis.d.ts.map +1 -0
- package/dist/legal-checks/gap-analysis.js +397 -0
- package/dist/legal-checks/gpai-classifier.d.ts +49 -0
- package/dist/legal-checks/gpai-classifier.d.ts.map +1 -0
- package/dist/legal-checks/gpai-classifier.js +197 -0
- package/dist/legal-checks/index.d.ts +6 -0
- package/dist/legal-checks/index.d.ts.map +1 -1
- package/dist/legal-checks/index.js +3 -0
- package/package.json +1 -1
- package/src/index.ts +25 -0
- package/src/inference/ollama-client.ts +21 -5
- package/src/legal-checks/annex-iv-report.ts +4 -0
- package/src/legal-checks/deadline-timeline.ts +313 -0
- package/src/legal-checks/gap-analysis.ts +454 -0
- package/src/legal-checks/gpai-classifier.ts +281 -0
- package/src/legal-checks/index.ts +33 -0
- package/tests/gpai-classifier.test.ts +125 -0
- package/tests/gpai-timeline.test.ts +55 -0
- package/.turbo/turbo-check-types.log +0 -4
- package/.turbo/turbo-test.log +0 -364
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/clover.xml +0 -3282
- package/coverage/coverage-final.json +0 -20
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +0 -161
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -210
- package/coverage/src/fairness-metrics.ts.html +0 -1579
- package/coverage/src/index.html +0 -161
- package/coverage/src/index.ts.html +0 -535
- package/coverage/src/inference/browser-inference.ts.html +0 -859
- package/coverage/src/inference/capability-detection.ts.html +0 -2227
- package/coverage/src/inference/index.html +0 -176
- package/coverage/src/inference/index.ts.html +0 -553
- package/coverage/src/inference/inference-factory.ts.html +0 -751
- package/coverage/src/inference/ollama-client.ts.html +0 -1627
- package/coverage/src/legal-checks/annex-iv-report.ts.html +0 -556
- package/coverage/src/legal-checks/bias-metrics.ts.html +0 -1855
- package/coverage/src/legal-checks/data-governance.ts.html +0 -529
- package/coverage/src/legal-checks/human-oversight-logging.ts.html +0 -490
- package/coverage/src/legal-checks/index.html +0 -206
- package/coverage/src/legal-checks/index.ts.html +0 -250
- package/coverage/src/legal-checks/risk-classifier.ts.html +0 -940
- package/coverage/src/legal-checks/robustness-transparency.ts.html +0 -571
- package/coverage/src/metrics/crows-pairs-bias.ts.html +0 -820
- package/coverage/src/metrics/index.html +0 -146
- package/coverage/src/metrics/index.ts.html +0 -151
- package/coverage/src/metrics/stereotype-bias.ts.html +0 -886
- package/coverage/src/risk-engine.ts.html +0 -1660
- package/coverage/src/types.ts.html +0 -547
- package/dist/legal-checks/disclaimer.d.ts +0 -2
- package/dist/legal-checks/disclaimer.d.ts.map +0 -1
- package/dist/legal-checks/disclaimer.js +0 -1
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GPAI / Foundation Model Compliance Classifier
|
|
3
|
+
* Based on EU AI Act (Regulation EU 2024/1689) – Art. 53–55 + Art. 51 + Art. 71
|
|
4
|
+
*
|
|
5
|
+
* Obligations enforceable since August 2, 2025 (Phase 2, Art. 113(2)).
|
|
6
|
+
*
|
|
7
|
+
* IMPORTANT: This module provides technical orientation, not legal advice.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { QuizAnswer, QuizQuestion } from "../types";
|
|
11
|
+
|
|
12
|
+
/** The specific obligations checked under Art. 53–55 */
|
|
13
|
+
export type GPAIObligationType =
|
|
14
|
+
| "technical-docs"
|
|
15
|
+
| "downstream-info"
|
|
16
|
+
| "copyright-policy"
|
|
17
|
+
| "eu-database"
|
|
18
|
+
| "open-source"
|
|
19
|
+
| "systemic-risk"
|
|
20
|
+
| "red-teaming"
|
|
21
|
+
| "incident-reporting"
|
|
22
|
+
| "cybersecurity";
|
|
23
|
+
|
|
24
|
+
/** Overall compliance determination */
|
|
25
|
+
export type GPAIComplianceLevel = "compliant" | "partial" | "non-compliant";
|
|
26
|
+
|
|
27
|
+
/** A specific obligation that is missing or only partially met */
|
|
28
|
+
export interface GPAIComplianceFlag {
|
|
29
|
+
obligation: GPAIObligationType;
|
|
30
|
+
status: "missing" | "partial";
|
|
31
|
+
articleRef: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Full result of GPAI compliance classification */
|
|
35
|
+
export interface GPAIComplianceResult {
|
|
36
|
+
level: GPAIComplianceLevel;
|
|
37
|
+
isSystemicRisk: boolean;
|
|
38
|
+
flags: GPAIComplianceFlag[];
|
|
39
|
+
legalBasis: string[];
|
|
40
|
+
notes: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The 9 GPAI compliance questions (Art. 53–55).
|
|
45
|
+
* Order matters: q1–q9 map to i18n keys gpai_q1–gpai_q9.
|
|
46
|
+
*/
|
|
47
|
+
export const GPAI_QUESTIONS: QuizQuestion[] = [
|
|
48
|
+
{
|
|
49
|
+
id: "gpai-technical-docs",
|
|
50
|
+
question: "Art. 53(1)(a): Technical documentation (Annex XI/XII)?",
|
|
51
|
+
description:
|
|
52
|
+
"Have you prepared and maintain up-to-date technical documentation for the GPAI model per Annex XI/XII (general-purpose) or the systemic-risk extension?",
|
|
53
|
+
options: [
|
|
54
|
+
{ value: "yes", label: "Yes, implemented" },
|
|
55
|
+
{ value: "no", label: "No, missing" },
|
|
56
|
+
{ value: "unsure", label: "In progress / unsure" },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "gpai-downstream-info",
|
|
61
|
+
question: "Art. 53(1)(b): Downstream provider information?",
|
|
62
|
+
description:
|
|
63
|
+
"Do you provide downstream providers / deployers with the information they need to comply with their own EU AI Act obligations (capabilities, limitations, intended use)?",
|
|
64
|
+
options: [
|
|
65
|
+
{ value: "yes", label: "Yes, implemented" },
|
|
66
|
+
{ value: "no", label: "No, missing" },
|
|
67
|
+
{ value: "unsure", label: "In progress / unsure" },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "gpai-copyright-policy",
|
|
72
|
+
question: "Art. 53(1)(c–d): Copyright policy + training data summary?",
|
|
73
|
+
description:
|
|
74
|
+
"Have you established a copyright compliance policy (Art. 53(1)(c)) and published a sufficiently detailed training data summary for copyright purposes (Art. 53(1)(d))?",
|
|
75
|
+
options: [
|
|
76
|
+
{ value: "yes", label: "Yes, implemented" },
|
|
77
|
+
{ value: "no", label: "No, missing" },
|
|
78
|
+
{ value: "unsure", label: "In progress / unsure" },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "gpai-eu-database",
|
|
83
|
+
question: "Art. 71: EU AI Database registration?",
|
|
84
|
+
description:
|
|
85
|
+
"Is the GPAI model registered in the EU AI Database as required under Art. 71 (providers of GPAI models must register before placing the model on the EU market)?",
|
|
86
|
+
options: [
|
|
87
|
+
{ value: "yes", label: "Yes, registered" },
|
|
88
|
+
{ value: "no", label: "No, not registered" },
|
|
89
|
+
{ value: "unsure", label: "In progress / unsure" },
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "gpai-open-source",
|
|
94
|
+
question: "Art. 53(2): Open-source model release?",
|
|
95
|
+
description:
|
|
96
|
+
"Is the model released as open source with weights publicly available? Open-source GPAI models are exempt from Art. 53(1)(a–c) obligations (technical docs, downstream info, copyright policy) but not from Art. 53(1)(d) (training data summary) or Art. 55 systemic-risk obligations.",
|
|
97
|
+
options: [
|
|
98
|
+
{ value: "yes", label: "Yes, open source" },
|
|
99
|
+
{ value: "no", label: "No, proprietary" },
|
|
100
|
+
{ value: "unsure", label: "Unsure / partially open" },
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "gpai-systemic-risk",
|
|
105
|
+
question: "Art. 51 + Annex XIII: Trained with ≥ 10²⁵ FLOP?",
|
|
106
|
+
description:
|
|
107
|
+
"Was the model trained using compute exceeding 10²⁵ floating-point operations (FLOP)? Models at or above this threshold are designated as GPAI models with systemic risk (Art. 51) and are subject to the additional Art. 55 obligations below.",
|
|
108
|
+
options: [
|
|
109
|
+
{ value: "yes", label: "Yes (systemic risk)" },
|
|
110
|
+
{ value: "no", label: "No (below threshold)" },
|
|
111
|
+
{ value: "unsure", label: "Unknown / unsure" },
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: "gpai-red-teaming",
|
|
116
|
+
question: "Art. 55(1)(a): Adversarial testing / red-teaming?",
|
|
117
|
+
description:
|
|
118
|
+
"Have you conducted model evaluations, adversarial testing, and red-teaming to identify and mitigate systemic risks (required only for Art. 51 systemic-risk models)?",
|
|
119
|
+
options: [
|
|
120
|
+
{ value: "yes", label: "Yes, conducted" },
|
|
121
|
+
{ value: "no", label: "No, not done" },
|
|
122
|
+
{ value: "unsure", label: "In progress / unsure" },
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "gpai-incident-reporting",
|
|
127
|
+
question: "Art. 55(1)(b): Serious incident reporting mechanisms?",
|
|
128
|
+
description:
|
|
129
|
+
"Do you have mechanisms to track, document, and report serious incidents caused by the GPAI model to the AI Office and affected national authorities (required only for Art. 51 systemic-risk models)?",
|
|
130
|
+
options: [
|
|
131
|
+
{ value: "yes", label: "Yes, in place" },
|
|
132
|
+
{ value: "no", label: "No, missing" },
|
|
133
|
+
{ value: "unsure", label: "In progress / unsure" },
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: "gpai-cybersecurity",
|
|
138
|
+
question: "Art. 55(1)(c): Cybersecurity measures?",
|
|
139
|
+
description:
|
|
140
|
+
"Have you implemented adequate cybersecurity protections for the GPAI model and its infrastructure, including adversarial robustness measures (required only for Art. 51 systemic-risk models)?",
|
|
141
|
+
options: [
|
|
142
|
+
{ value: "yes", label: "Yes, implemented" },
|
|
143
|
+
{ value: "no", label: "No, missing" },
|
|
144
|
+
{ value: "unsure", label: "In progress / unsure" },
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
/** Returns the full ordered list of GPAI compliance questions */
|
|
150
|
+
export function getGPAIQuestions(): QuizQuestion[] {
|
|
151
|
+
return GPAI_QUESTIONS;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Returns true if the model meets the systemic risk threshold (Art. 51) */
|
|
155
|
+
export function isSystemicRiskModel(answers: QuizAnswer[]): boolean {
|
|
156
|
+
const systemicAnswer = answers.find((a) => a.questionId === "gpai-systemic-risk");
|
|
157
|
+
return systemicAnswer?.value === "yes";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Push a flag if the answer value is "no" or "unsure" */
|
|
161
|
+
function checkObligation(
|
|
162
|
+
value: string | undefined,
|
|
163
|
+
obligation: GPAIObligationType,
|
|
164
|
+
articleRef: string,
|
|
165
|
+
flags: GPAIComplianceFlag[]
|
|
166
|
+
): void {
|
|
167
|
+
if (value === "no") {
|
|
168
|
+
flags.push({ obligation, status: "missing", articleRef });
|
|
169
|
+
} else if (value === "unsure") {
|
|
170
|
+
flags.push({ obligation, status: "partial", articleRef });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Evaluate Art. 53 obligations and populate flags / notes */
|
|
175
|
+
function evaluateArt53Obligations(
|
|
176
|
+
byId: Map<string, string>,
|
|
177
|
+
isOpenSource: boolean,
|
|
178
|
+
flags: GPAIComplianceFlag[],
|
|
179
|
+
legalBasis: string[],
|
|
180
|
+
notes: string[]
|
|
181
|
+
): void {
|
|
182
|
+
if (!isOpenSource) {
|
|
183
|
+
checkObligation(byId.get("gpai-technical-docs"), "technical-docs", "Art. 53(1)(a)", flags);
|
|
184
|
+
checkObligation(byId.get("gpai-downstream-info"), "downstream-info", "Art. 53(1)(b)", flags);
|
|
185
|
+
checkObligation(
|
|
186
|
+
byId.get("gpai-copyright-policy"),
|
|
187
|
+
"copyright-policy",
|
|
188
|
+
"Art. 53(1)(c–d)",
|
|
189
|
+
flags
|
|
190
|
+
);
|
|
191
|
+
} else {
|
|
192
|
+
// Open-source: Art. 53(1)(a–c) exempt, but training data summary (Art. 53(1)(d)) still applies
|
|
193
|
+
const copyrightVal = byId.get("gpai-copyright-policy");
|
|
194
|
+
checkObligation(copyrightVal, "copyright-policy", "Art. 53(1)(d)", flags);
|
|
195
|
+
if (copyrightVal === "no" || copyrightVal === "unsure") {
|
|
196
|
+
notes.push(
|
|
197
|
+
"Open-source models are exempt from Art. 53(1)(c) copyright policy obligations, but Art. 53(1)(d) training data summary still applies."
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const euDatabase = byId.get("gpai-eu-database");
|
|
203
|
+
checkObligation(euDatabase, "eu-database", "Art. 71", flags);
|
|
204
|
+
if (euDatabase === "no") {
|
|
205
|
+
legalBasis.push("Verordnung (EU) 2024/1689 – Art. 71 (EU AI Database)");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Evaluate Art. 55 systemic risk obligations and populate flags */
|
|
210
|
+
function evaluateArt55Obligations(
|
|
211
|
+
byId: Map<string, string>,
|
|
212
|
+
flags: GPAIComplianceFlag[],
|
|
213
|
+
legalBasis: string[]
|
|
214
|
+
): void {
|
|
215
|
+
legalBasis.push("Verordnung (EU) 2024/1689 – Art. 51 + Art. 55 (Systemic Risk)");
|
|
216
|
+
legalBasis.push("Verordnung (EU) 2024/1689 – Annex XIII (Systemic Risk Indicators)");
|
|
217
|
+
checkObligation(byId.get("gpai-red-teaming"), "red-teaming", "Art. 55(1)(a)", flags);
|
|
218
|
+
checkObligation(
|
|
219
|
+
byId.get("gpai-incident-reporting"),
|
|
220
|
+
"incident-reporting",
|
|
221
|
+
"Art. 55(1)(b)",
|
|
222
|
+
flags
|
|
223
|
+
);
|
|
224
|
+
checkObligation(byId.get("gpai-cybersecurity"), "cybersecurity", "Art. 55(1)(c)", flags);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Determine overall compliance level from accumulated flags */
|
|
228
|
+
function determineLevel(flags: GPAIComplianceFlag[], isOpenSource: boolean): GPAIComplianceLevel {
|
|
229
|
+
const criticalObligations: GPAIObligationType[] = isOpenSource
|
|
230
|
+
? ["eu-database"]
|
|
231
|
+
: ["technical-docs", "downstream-info", "copyright-policy", "eu-database"];
|
|
232
|
+
|
|
233
|
+
const allCriticalMissing =
|
|
234
|
+
!isOpenSource &&
|
|
235
|
+
criticalObligations.every((obl) =>
|
|
236
|
+
flags.some((f) => f.obligation === obl && f.status === "missing")
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
if (allCriticalMissing) return "non-compliant";
|
|
240
|
+
if (flags.length > 0) return "partial";
|
|
241
|
+
return "compliant";
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Classify GPAI compliance based on answers to the 9 questions.
|
|
246
|
+
* Returns a GPAIComplianceResult with level, flags, and legal basis.
|
|
247
|
+
*
|
|
248
|
+
* Classification logic:
|
|
249
|
+
* - Open-source (Art. 53(2)): exempt from docs/downstream/copyright obligations
|
|
250
|
+
* - Systemic risk (Art. 51): Art. 55 obligations apply
|
|
251
|
+
* - Critical Art. 53 all missing → "non-compliant"
|
|
252
|
+
* - Any "unsure" or partial → "partial"
|
|
253
|
+
* - All applicable "yes" → "compliant"
|
|
254
|
+
*/
|
|
255
|
+
export function classifyGPAICompliance(answers: QuizAnswer[]): GPAIComplianceResult {
|
|
256
|
+
const byId = new Map(answers.map((a) => [a.questionId, a.value] as const));
|
|
257
|
+
const isOpenSource = byId.get("gpai-open-source") === "yes";
|
|
258
|
+
const hasSystemicRisk = byId.get("gpai-systemic-risk") === "yes";
|
|
259
|
+
|
|
260
|
+
const flags: GPAIComplianceFlag[] = [];
|
|
261
|
+
const legalBasis: string[] = ["Verordnung (EU) 2024/1689 – Art. 53 (GPAI obligations)"];
|
|
262
|
+
const notes: string[] = [];
|
|
263
|
+
|
|
264
|
+
evaluateArt53Obligations(byId, isOpenSource, flags, legalBasis, notes);
|
|
265
|
+
|
|
266
|
+
if (hasSystemicRisk) {
|
|
267
|
+
evaluateArt55Obligations(byId, flags, legalBasis);
|
|
268
|
+
} else {
|
|
269
|
+
notes.push(
|
|
270
|
+
"Art. 55 systemic risk obligations (red-teaming, incident reporting, cybersecurity) are not applicable as the model does not meet the Art. 51 threshold (≥ 10²⁵ FLOP)."
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
level: determineLevel(flags, isOpenSource),
|
|
276
|
+
isSystemicRisk: hasSystemicRisk,
|
|
277
|
+
flags,
|
|
278
|
+
legalBasis,
|
|
279
|
+
notes,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
@@ -53,3 +53,36 @@ export { getHumanOversightAndLoggingTemplate } from "./human-oversight-logging";
|
|
|
53
53
|
|
|
54
54
|
export type { AnnexIVStructuredReportV1 } from "./annex-iv-report";
|
|
55
55
|
export { buildAnnexIVReportV1 } from "./annex-iv-report";
|
|
56
|
+
|
|
57
|
+
export type {
|
|
58
|
+
DeadlineStatus,
|
|
59
|
+
AffectedAudience,
|
|
60
|
+
DeadlineObligation,
|
|
61
|
+
DeadlinePhase,
|
|
62
|
+
} from "./deadline-timeline";
|
|
63
|
+
export {
|
|
64
|
+
EU_AI_ACT_TIMELINE,
|
|
65
|
+
DIGITAL_OMNIBUS_PROPOSALS,
|
|
66
|
+
getDeadlineStatus,
|
|
67
|
+
getDaysUntilDeadline,
|
|
68
|
+
getAudiencesForRiskLevel,
|
|
69
|
+
getNextDeadline,
|
|
70
|
+
getTimelineForRiskLevel,
|
|
71
|
+
getTimelineForAudiences,
|
|
72
|
+
} from "./deadline-timeline";
|
|
73
|
+
|
|
74
|
+
export type {
|
|
75
|
+
GPAIObligationType,
|
|
76
|
+
GPAIComplianceLevel,
|
|
77
|
+
GPAIComplianceFlag,
|
|
78
|
+
GPAIComplianceResult,
|
|
79
|
+
} from "./gpai-classifier";
|
|
80
|
+
export {
|
|
81
|
+
GPAI_QUESTIONS,
|
|
82
|
+
classifyGPAICompliance,
|
|
83
|
+
getGPAIQuestions,
|
|
84
|
+
isSystemicRiskModel,
|
|
85
|
+
} from "./gpai-classifier";
|
|
86
|
+
|
|
87
|
+
export type { GapPriority, GapAction, GapAnalysisResult } from "./gap-analysis";
|
|
88
|
+
export { generateAnnexIIIGapAnalysis, generateGPAIGapAnalysis } from "./gap-analysis";
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { classifyGPAICompliance, isSystemicRiskModel } from "../src/legal-checks/gpai-classifier";
|
|
3
|
+
import type { QuizAnswer } from "../src/types";
|
|
4
|
+
|
|
5
|
+
/** Helper: build a full set of GPAI answers with defaults, then override specific ones */
|
|
6
|
+
function buildAnswers(overrides: Record<string, string> = {}): QuizAnswer[] {
|
|
7
|
+
const defaults: Record<string, string> = {
|
|
8
|
+
"gpai-technical-docs": "yes",
|
|
9
|
+
"gpai-downstream-info": "yes",
|
|
10
|
+
"gpai-copyright-policy": "yes",
|
|
11
|
+
"gpai-eu-database": "yes",
|
|
12
|
+
"gpai-open-source": "no",
|
|
13
|
+
"gpai-systemic-risk": "no",
|
|
14
|
+
"gpai-red-teaming": "yes",
|
|
15
|
+
"gpai-incident-reporting": "yes",
|
|
16
|
+
"gpai-cybersecurity": "yes",
|
|
17
|
+
};
|
|
18
|
+
const merged = { ...defaults, ...overrides };
|
|
19
|
+
return Object.entries(merged).map(([questionId, value]) => ({
|
|
20
|
+
questionId,
|
|
21
|
+
value,
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("classifyGPAICompliance", () => {
|
|
26
|
+
describe("open-source path (Art. 53(2))", () => {
|
|
27
|
+
it("flags copyright-policy as missing when open-source answers 'no'", () => {
|
|
28
|
+
const answers = buildAnswers({
|
|
29
|
+
"gpai-open-source": "yes",
|
|
30
|
+
"gpai-copyright-policy": "no",
|
|
31
|
+
});
|
|
32
|
+
const result = classifyGPAICompliance(answers);
|
|
33
|
+
const flag = result.flags.find((f) => f.obligation === "copyright-policy");
|
|
34
|
+
expect(flag).toBeDefined();
|
|
35
|
+
expect(flag!.status).toBe("missing");
|
|
36
|
+
expect(flag!.articleRef).toBe("Art. 53(1)(d)");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("flags copyright-policy as partial when open-source answers 'unsure'", () => {
|
|
40
|
+
const answers = buildAnswers({
|
|
41
|
+
"gpai-open-source": "yes",
|
|
42
|
+
"gpai-copyright-policy": "unsure",
|
|
43
|
+
});
|
|
44
|
+
const result = classifyGPAICompliance(answers);
|
|
45
|
+
const flag = result.flags.find((f) => f.obligation === "copyright-policy");
|
|
46
|
+
expect(flag).toBeDefined();
|
|
47
|
+
expect(flag!.status).toBe("partial");
|
|
48
|
+
expect(flag!.articleRef).toBe("Art. 53(1)(d)");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("does NOT flag copyright-policy when open-source answers 'yes'", () => {
|
|
52
|
+
const answers = buildAnswers({
|
|
53
|
+
"gpai-open-source": "yes",
|
|
54
|
+
"gpai-copyright-policy": "yes",
|
|
55
|
+
});
|
|
56
|
+
const result = classifyGPAICompliance(answers);
|
|
57
|
+
const flag = result.flags.find((f) => f.obligation === "copyright-policy");
|
|
58
|
+
expect(flag).toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("exempts technical-docs and downstream-info for open-source", () => {
|
|
62
|
+
const answers = buildAnswers({
|
|
63
|
+
"gpai-open-source": "yes",
|
|
64
|
+
"gpai-technical-docs": "no",
|
|
65
|
+
"gpai-downstream-info": "no",
|
|
66
|
+
});
|
|
67
|
+
const result = classifyGPAICompliance(answers);
|
|
68
|
+
expect(result.flags.find((f) => f.obligation === "technical-docs")).toBeUndefined();
|
|
69
|
+
expect(result.flags.find((f) => f.obligation === "downstream-info")).toBeUndefined();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("includes explanatory note for open-source copyright gap", () => {
|
|
73
|
+
const answers = buildAnswers({
|
|
74
|
+
"gpai-open-source": "yes",
|
|
75
|
+
"gpai-copyright-policy": "unsure",
|
|
76
|
+
});
|
|
77
|
+
const result = classifyGPAICompliance(answers);
|
|
78
|
+
expect(result.notes.some((n) => n.includes("Art. 53(1)(d)"))).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("non-open-source path", () => {
|
|
83
|
+
it("flags all three Art. 53(1) obligations when missing", () => {
|
|
84
|
+
const answers = buildAnswers({
|
|
85
|
+
"gpai-technical-docs": "no",
|
|
86
|
+
"gpai-downstream-info": "no",
|
|
87
|
+
"gpai-copyright-policy": "no",
|
|
88
|
+
"gpai-eu-database": "no",
|
|
89
|
+
});
|
|
90
|
+
const result = classifyGPAICompliance(answers);
|
|
91
|
+
expect(result.level).toBe("non-compliant");
|
|
92
|
+
expect(result.flags.length).toBeGreaterThanOrEqual(4);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("systemic risk (Art. 51)", () => {
|
|
97
|
+
it("detects systemic risk model", () => {
|
|
98
|
+
const answers = buildAnswers({ "gpai-systemic-risk": "yes" });
|
|
99
|
+
expect(isSystemicRiskModel(answers)).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("flags Art. 55 obligations when systemic risk and missing", () => {
|
|
103
|
+
const answers = buildAnswers({
|
|
104
|
+
"gpai-systemic-risk": "yes",
|
|
105
|
+
"gpai-red-teaming": "no",
|
|
106
|
+
"gpai-incident-reporting": "no",
|
|
107
|
+
"gpai-cybersecurity": "no",
|
|
108
|
+
});
|
|
109
|
+
const result = classifyGPAICompliance(answers);
|
|
110
|
+
expect(result.isSystemicRisk).toBe(true);
|
|
111
|
+
expect(result.flags.find((f) => f.obligation === "red-teaming")).toBeDefined();
|
|
112
|
+
expect(result.flags.find((f) => f.obligation === "incident-reporting")).toBeDefined();
|
|
113
|
+
expect(result.flags.find((f) => f.obligation === "cybersecurity")).toBeDefined();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("fully compliant", () => {
|
|
118
|
+
it("returns compliant with no flags when all obligations met", () => {
|
|
119
|
+
const answers = buildAnswers();
|
|
120
|
+
const result = classifyGPAICompliance(answers);
|
|
121
|
+
expect(result.level).toBe("compliant");
|
|
122
|
+
expect(result.flags).toHaveLength(0);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { getTimelineForRiskLevel } from "../src/legal-checks/deadline-timeline";
|
|
3
|
+
|
|
4
|
+
// This function will be implemented in Step 3
|
|
5
|
+
import { getTimelineForAudiences } from "../src/legal-checks/deadline-timeline";
|
|
6
|
+
|
|
7
|
+
describe("getTimelineForAudiences", () => {
|
|
8
|
+
it("returns GPAI-relevant phases for ['all', 'gpai'] audiences", () => {
|
|
9
|
+
const phases = getTimelineForAudiences(["all", "gpai"]);
|
|
10
|
+
const phaseIds = phases.map((p) => p.id);
|
|
11
|
+
expect(phaseIds).toContain("phase-2-gpai");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("includes Art. 53 documentation obligation for GPAI audience", () => {
|
|
15
|
+
const phases = getTimelineForAudiences(["all", "gpai"]);
|
|
16
|
+
const phase2 = phases.find((p) => p.id === "phase-2-gpai");
|
|
17
|
+
expect(phase2).toBeDefined();
|
|
18
|
+
const gpaiObligation = phase2!.obligations.find((o) => o.articleRef === "Art. 53");
|
|
19
|
+
expect(gpaiObligation).toBeDefined();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("includes Art. 55 systemic risk obligation for GPAI audience", () => {
|
|
23
|
+
const phases = getTimelineForAudiences(["all", "gpai"]);
|
|
24
|
+
const phase2 = phases.find((p) => p.id === "phase-2-gpai");
|
|
25
|
+
expect(phase2).toBeDefined();
|
|
26
|
+
const systemicObligation = phase2!.obligations.find((o) => o.articleRef === "Art. 55");
|
|
27
|
+
expect(systemicObligation).toBeDefined();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("does NOT include high-risk Annex III obligations for GPAI audience", () => {
|
|
31
|
+
const phases = getTimelineForAudiences(["all", "gpai"]);
|
|
32
|
+
const phase3 = phases.find((p) => p.id === "phase-3-high-risk-annex-iii");
|
|
33
|
+
if (phase3) {
|
|
34
|
+
const hasAllAudience = phase3.obligations.some((o) =>
|
|
35
|
+
o.audiences.some((a) => ["all", "gpai"].includes(a))
|
|
36
|
+
);
|
|
37
|
+
expect(hasAllAudience).toBe(true);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("minimal risk level does NOT include GPAI obligations (regression)", () => {
|
|
42
|
+
const phases = getTimelineForRiskLevel("minimal");
|
|
43
|
+
const phase2 = phases.find((p) => p.id === "phase-2-gpai");
|
|
44
|
+
if (phase2) {
|
|
45
|
+
const gpaiOnly = phase2.obligations.filter(
|
|
46
|
+
(o) => o.audiences.includes("gpai") && !o.audiences.includes("all")
|
|
47
|
+
);
|
|
48
|
+
const minimalAudiences = ["all", "minimal"];
|
|
49
|
+
for (const obl of gpaiOnly) {
|
|
50
|
+
const matches = obl.audiences.some((a) => minimalAudiences.includes(a));
|
|
51
|
+
expect(matches).toBe(false);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|