@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.
Files changed (71) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +6 -0
  3. package/dist/index.d.ts +2 -2
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/inference/ollama-client.d.ts +1 -0
  7. package/dist/inference/ollama-client.d.ts.map +1 -1
  8. package/dist/inference/ollama-client.js +15 -5
  9. package/dist/legal-checks/annex-iv-report.d.ts +3 -0
  10. package/dist/legal-checks/annex-iv-report.d.ts.map +1 -1
  11. package/dist/legal-checks/annex-iv-report.js +1 -0
  12. package/dist/legal-checks/deadline-timeline.d.ts +57 -0
  13. package/dist/legal-checks/deadline-timeline.d.ts.map +1 -0
  14. package/dist/legal-checks/deadline-timeline.js +265 -0
  15. package/dist/legal-checks/gap-analysis.d.ts +48 -0
  16. package/dist/legal-checks/gap-analysis.d.ts.map +1 -0
  17. package/dist/legal-checks/gap-analysis.js +397 -0
  18. package/dist/legal-checks/gpai-classifier.d.ts +49 -0
  19. package/dist/legal-checks/gpai-classifier.d.ts.map +1 -0
  20. package/dist/legal-checks/gpai-classifier.js +197 -0
  21. package/dist/legal-checks/index.d.ts +6 -0
  22. package/dist/legal-checks/index.d.ts.map +1 -1
  23. package/dist/legal-checks/index.js +3 -0
  24. package/package.json +1 -1
  25. package/src/index.ts +25 -0
  26. package/src/inference/ollama-client.ts +21 -5
  27. package/src/legal-checks/annex-iv-report.ts +4 -0
  28. package/src/legal-checks/deadline-timeline.ts +313 -0
  29. package/src/legal-checks/gap-analysis.ts +454 -0
  30. package/src/legal-checks/gpai-classifier.ts +281 -0
  31. package/src/legal-checks/index.ts +33 -0
  32. package/tests/gpai-classifier.test.ts +125 -0
  33. package/tests/gpai-timeline.test.ts +55 -0
  34. package/.turbo/turbo-check-types.log +0 -4
  35. package/.turbo/turbo-test.log +0 -364
  36. package/coverage/base.css +0 -224
  37. package/coverage/block-navigation.js +0 -87
  38. package/coverage/clover.xml +0 -3282
  39. package/coverage/coverage-final.json +0 -20
  40. package/coverage/favicon.png +0 -0
  41. package/coverage/index.html +0 -161
  42. package/coverage/prettify.css +0 -1
  43. package/coverage/prettify.js +0 -2
  44. package/coverage/sort-arrow-sprite.png +0 -0
  45. package/coverage/sorter.js +0 -210
  46. package/coverage/src/fairness-metrics.ts.html +0 -1579
  47. package/coverage/src/index.html +0 -161
  48. package/coverage/src/index.ts.html +0 -535
  49. package/coverage/src/inference/browser-inference.ts.html +0 -859
  50. package/coverage/src/inference/capability-detection.ts.html +0 -2227
  51. package/coverage/src/inference/index.html +0 -176
  52. package/coverage/src/inference/index.ts.html +0 -553
  53. package/coverage/src/inference/inference-factory.ts.html +0 -751
  54. package/coverage/src/inference/ollama-client.ts.html +0 -1627
  55. package/coverage/src/legal-checks/annex-iv-report.ts.html +0 -556
  56. package/coverage/src/legal-checks/bias-metrics.ts.html +0 -1855
  57. package/coverage/src/legal-checks/data-governance.ts.html +0 -529
  58. package/coverage/src/legal-checks/human-oversight-logging.ts.html +0 -490
  59. package/coverage/src/legal-checks/index.html +0 -206
  60. package/coverage/src/legal-checks/index.ts.html +0 -250
  61. package/coverage/src/legal-checks/risk-classifier.ts.html +0 -940
  62. package/coverage/src/legal-checks/robustness-transparency.ts.html +0 -571
  63. package/coverage/src/metrics/crows-pairs-bias.ts.html +0 -820
  64. package/coverage/src/metrics/index.html +0 -146
  65. package/coverage/src/metrics/index.ts.html +0 -151
  66. package/coverage/src/metrics/stereotype-bias.ts.html +0 -886
  67. package/coverage/src/risk-engine.ts.html +0 -1660
  68. package/coverage/src/types.ts.html +0 -547
  69. package/dist/legal-checks/disclaimer.d.ts +0 -2
  70. package/dist/legal-checks/disclaimer.d.ts.map +0 -1
  71. package/dist/legal-checks/disclaimer.js +0 -1
@@ -4,3 +4,6 @@ export { runDataGovernanceChecklist } from "./data-governance";
4
4
  export { defaultTextPerturbations, runTextPerturbationRobustnessTest, } from "./robustness-transparency";
5
5
  export { getHumanOversightAndLoggingTemplate } from "./human-oversight-logging";
6
6
  export { buildAnnexIVReportV1 } from "./annex-iv-report";
7
+ export { EU_AI_ACT_TIMELINE, DIGITAL_OMNIBUS_PROPOSALS, getDeadlineStatus, getDaysUntilDeadline, getAudiencesForRiskLevel, getNextDeadline, getTimelineForRiskLevel, getTimelineForAudiences, } from "./deadline-timeline";
8
+ export { GPAI_QUESTIONS, classifyGPAICompliance, getGPAIQuestions, isSystemicRiskModel, } from "./gpai-classifier";
9
+ export { generateAnnexIIIGapAnalysis, generateGPAIGapAnalysis } from "./gap-analysis";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@euconform/core",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "license": "MIT OR EUPL-1.2",
5
5
  "author": "Benedikt Hiepler",
6
6
  "description": "Core engine for EU AI Act compliance, risk classification, and fairness metrics.",
package/src/index.ts CHANGED
@@ -31,6 +31,12 @@ export {
31
31
  classifyAnnexIIIRisk,
32
32
  getAnnexIIIQuestions,
33
33
  getProhibitedAIScreeningQuestions,
34
+ GPAI_QUESTIONS,
35
+ classifyGPAICompliance,
36
+ getGPAIQuestions,
37
+ isSystemicRiskModel,
38
+ generateAnnexIIIGapAnalysis,
39
+ generateGPAIGapAnalysis,
34
40
  AI_ACT_SOURCES,
35
41
  METRIC_SOURCES,
36
42
  createPerspectiveApiToxicityScorer,
@@ -48,6 +54,14 @@ export {
48
54
  runCrowsPairsLogProbTest,
49
55
  runRegardTest,
50
56
  runToxicityTest,
57
+ EU_AI_ACT_TIMELINE,
58
+ DIGITAL_OMNIBUS_PROPOSALS,
59
+ getDeadlineStatus,
60
+ getDaysUntilDeadline,
61
+ getAudiencesForRiskLevel,
62
+ getNextDeadline,
63
+ getTimelineForRiskLevel,
64
+ getTimelineForAudiences,
51
65
  } from "./legal-checks";
52
66
  export type {
53
67
  AnnexIIIAnswer,
@@ -71,6 +85,17 @@ export type {
71
85
  SensitiveAttribute,
72
86
  ToxicityResult,
73
87
  ToxicityScorer,
88
+ DeadlineStatus,
89
+ AffectedAudience,
90
+ DeadlineObligation,
91
+ DeadlinePhase,
92
+ GPAIObligationType,
93
+ GPAIComplianceLevel,
94
+ GPAIComplianceFlag,
95
+ GPAIComplianceResult,
96
+ GapPriority,
97
+ GapAction,
98
+ GapAnalysisResult,
74
99
  } from "./legal-checks";
75
100
 
76
101
  // Risk Engine
@@ -224,6 +224,10 @@ export class OllamaClient {
224
224
  this.capabilityCache = new Map();
225
225
  }
226
226
 
227
+ private isCloudModel(): boolean {
228
+ return this.model.includes(":cloud");
229
+ }
230
+
227
231
  /**
228
232
  * Ensures the model is available in Ollama before proceeding
229
233
  * Throws an error if the model is not found
@@ -273,6 +277,14 @@ export class OllamaClient {
273
277
  * Sends a test request with "Der" prompt to detect capability
274
278
  */
275
279
  private async testLogProbCapability(): Promise<boolean> {
280
+ // Cloud models (via Ollama proxy) don't support logprobs parameter
281
+ if (this.isCloudModel()) {
282
+ console.log(
283
+ `🔍 ${this.model} is a cloud model - logprobs not supported, using latency method`
284
+ );
285
+ return false;
286
+ }
287
+
276
288
  try {
277
289
  // First check Ollama version
278
290
  try {
@@ -412,8 +424,7 @@ export class OllamaClient {
412
424
  // First, check if model supports log-probabilities
413
425
  const supportsLogProbs = await this.detectLogProbSupport();
414
426
 
415
- if (supportsLogProbs) {
416
- // Try to get exact log-probabilities
427
+ if (supportsLogProbs && !this.isCloudModel()) {
417
428
  try {
418
429
  const response = await fetch(`${this.baseUrl}/api/generate`, {
419
430
  method: "POST",
@@ -461,8 +472,12 @@ export class OllamaClient {
461
472
  }
462
473
 
463
474
  // Fallback to latency-based method
464
- console.log(`⚡ Falling back to latency-based method for ${this.model}`);
475
+ const isCloud = this.isCloudModel();
476
+ console.log(
477
+ `⚡ Falling back to latency-based method for ${this.model}${isCloud ? " (cloud model)" : ""}`
478
+ );
465
479
  const startTime = performance.now();
480
+ const numPredict = isCloud ? 1 : 0;
466
481
 
467
482
  const response = await fetch(`${this.baseUrl}/api/generate`, {
468
483
  method: "POST",
@@ -472,7 +487,7 @@ export class OllamaClient {
472
487
  prompt,
473
488
  stream: false,
474
489
  options: {
475
- num_predict: 0, // Only evaluate prompt
490
+ num_predict: numPredict,
476
491
  temperature: 0,
477
492
  },
478
493
  }),
@@ -481,7 +496,8 @@ export class OllamaClient {
481
496
  const endTime = performance.now();
482
497
 
483
498
  if (!response.ok) {
484
- throw new Error(`Ollama request failed: ${response.statusText}`);
499
+ const errorText = await response.text();
500
+ throw new Error(`Ollama request failed: ${response.statusText}. ${errorText}`);
485
501
  }
486
502
 
487
503
  const data = await response.json();
@@ -17,6 +17,7 @@
17
17
  import type { BiasCalculationMethod, BiasTestResult, RiskLevel } from "../types";
18
18
  import type { Citation } from "./bias-metrics";
19
19
  import type { DataGovernanceChecklistResult } from "./data-governance";
20
+ import type { GapAnalysisResult } from "./gap-analysis";
20
21
  import type { HumanOversightRecommendation } from "./human-oversight-logging";
21
22
  import type { AnnexIIIRiskResult } from "./risk-classifier";
22
23
 
@@ -88,6 +89,7 @@ export interface AnnexIVStructuredReportV1 {
88
89
  changeLog?: Array<{ date: string; change: string }>;
89
90
  references: Citation[];
90
91
  };
92
+ section8_gapAnalysis?: GapAnalysisResult;
91
93
  }
92
94
 
93
95
  export function buildAnnexIVReportV1(input: {
@@ -104,6 +106,7 @@ export function buildAnnexIVReportV1(input: {
104
106
  humanOversight?: AnnexIVStructuredReportV1["section6_humanOversight"];
105
107
  technicalDocumentation?: Partial<AnnexIVStructuredReportV1["section7_technicalDocumentation"]>;
106
108
  biasMethodology?: AnnexIVStructuredReportV1["section5_performanceAndFairness"]["biasMethodology"];
109
+ gapAnalysis?: GapAnalysisResult;
107
110
  tool?: { name: string; version?: string };
108
111
  disclaimer?: string;
109
112
  }): AnnexIVStructuredReportV1 {
@@ -153,5 +156,6 @@ export function buildAnnexIVReportV1(input: {
153
156
  ],
154
157
  references: input.technicalDocumentation?.references ?? input.legalSources,
155
158
  },
159
+ section8_gapAnalysis: input.gapAnalysis,
156
160
  };
157
161
  }
@@ -0,0 +1,313 @@
1
+ /**
2
+ * EU AI Act Compliance Deadline Timeline
3
+ * Based on Regulation (EU) 2024/1689, Article 113
4
+ *
5
+ * Non-legal-advice: These dates are extracted from the official regulation text
6
+ * and publicly available EU Commission sources. Always verify with legal counsel.
7
+ */
8
+
9
+ import type { RiskLevel } from "../types";
10
+
11
+ export type DeadlineStatus = "past" | "imminent" | "upcoming" | "future";
12
+
13
+ export type AffectedAudience =
14
+ | "all"
15
+ | "prohibited"
16
+ | "gpai"
17
+ | "high-annex-iii"
18
+ | "high-annex-i"
19
+ | "limited"
20
+ | "minimal";
21
+
22
+ export interface DeadlineObligation {
23
+ titleKey: string;
24
+ articleRef: string;
25
+ audiences: AffectedAudience[];
26
+ maxPenalty?: string;
27
+ proposed?: boolean;
28
+ }
29
+
30
+ export interface DeadlinePhase {
31
+ id: string;
32
+ isoDate: string;
33
+ chapterRef: string;
34
+ titleKey: string;
35
+ subtitleKey: string;
36
+ obligations: DeadlineObligation[];
37
+ status?: DeadlineStatus;
38
+ proposed?: boolean;
39
+ }
40
+
41
+ const MS_PER_DAY = 1000 * 60 * 60 * 24;
42
+
43
+ export function getDeadlineStatus(isoDate: string, nowMs = Date.now()): DeadlineStatus {
44
+ const diffDays = (new Date(isoDate).getTime() - nowMs) / MS_PER_DAY;
45
+ if (diffDays < 0) return "past";
46
+ if (diffDays <= 90) return "imminent";
47
+ if (diffDays <= 365) return "upcoming";
48
+ return "future";
49
+ }
50
+
51
+ export function getDaysUntilDeadline(isoDate: string, nowMs = Date.now()): number {
52
+ return Math.round((new Date(isoDate).getTime() - nowMs) / MS_PER_DAY);
53
+ }
54
+
55
+ /**
56
+ * EU AI Act Timeline Milestones – Art. 113 Application Schedule
57
+ *
58
+ * Sources:
59
+ * - Regulation (EU) 2024/1689, Article 113
60
+ * - EU Commission AI Act Service Desk: https://ai-act-service-desk.ec.europa.eu
61
+ * - Digital Omnibus Proposal, November 2025 (proposed, not yet law)
62
+ */
63
+ export const EU_AI_ACT_TIMELINE: DeadlinePhase[] = [
64
+ {
65
+ id: "entry-into-force",
66
+ isoDate: "2024-08-01",
67
+ chapterRef: "Art. 113",
68
+ titleKey: "timeline_phase0_title",
69
+ subtitleKey: "timeline_phase0_subtitle",
70
+ obligations: [
71
+ {
72
+ titleKey: "timeline_obligation_regulation_published",
73
+ articleRef: "Art. 113",
74
+ audiences: ["all"],
75
+ },
76
+ ],
77
+ },
78
+ {
79
+ id: "phase-1-prohibited",
80
+ isoDate: "2025-02-02",
81
+ chapterRef: "Art. 113(1) – Chapter I & II",
82
+ titleKey: "timeline_phase1_title",
83
+ subtitleKey: "timeline_phase1_subtitle",
84
+ obligations: [
85
+ {
86
+ titleKey: "timeline_obligation_definitions",
87
+ articleRef: "Art. 1–4",
88
+ audiences: ["all"],
89
+ },
90
+ {
91
+ titleKey: "timeline_obligation_prohibited",
92
+ articleRef: "Art. 5",
93
+ audiences: ["all", "prohibited"],
94
+ maxPenalty: "€35M / 7%",
95
+ },
96
+ {
97
+ titleKey: "timeline_obligation_ai_literacy",
98
+ articleRef: "Art. 4",
99
+ audiences: ["all"],
100
+ },
101
+ ],
102
+ },
103
+ {
104
+ id: "phase-2-gpai",
105
+ isoDate: "2025-08-02",
106
+ chapterRef: "Art. 113(2) – Chapter V, VII, XII",
107
+ titleKey: "timeline_phase2_title",
108
+ subtitleKey: "timeline_phase2_subtitle",
109
+ obligations: [
110
+ {
111
+ titleKey: "timeline_obligation_gpai_documentation",
112
+ articleRef: "Art. 53",
113
+ audiences: ["gpai"],
114
+ maxPenalty: "€15M / 3%",
115
+ },
116
+ {
117
+ titleKey: "timeline_obligation_gpai_copyright",
118
+ articleRef: "Art. 53(1)(c)",
119
+ audiences: ["gpai"],
120
+ },
121
+ {
122
+ titleKey: "timeline_obligation_gpai_systemic_risk",
123
+ articleRef: "Art. 55",
124
+ audiences: ["gpai"],
125
+ maxPenalty: "€15M / 3%",
126
+ },
127
+ {
128
+ titleKey: "timeline_obligation_notified_bodies",
129
+ articleRef: "Art. 28–39",
130
+ audiences: ["high-annex-iii", "high-annex-i"],
131
+ },
132
+ {
133
+ titleKey: "timeline_obligation_governance",
134
+ articleRef: "Art. 64–70",
135
+ audiences: ["all"],
136
+ },
137
+ {
138
+ titleKey: "timeline_obligation_penalties_framework",
139
+ articleRef: "Art. 99–100",
140
+ audiences: ["all"],
141
+ },
142
+ ],
143
+ },
144
+ {
145
+ id: "phase-3-high-risk-annex-iii",
146
+ isoDate: "2026-08-02",
147
+ chapterRef: "Art. 113(3) – Full Application",
148
+ titleKey: "timeline_phase3_title",
149
+ subtitleKey: "timeline_phase3_subtitle",
150
+ obligations: [
151
+ {
152
+ titleKey: "timeline_obligation_high_risk_requirements",
153
+ articleRef: "Art. 8–25",
154
+ audiences: ["high-annex-iii"],
155
+ maxPenalty: "€15M / 3%",
156
+ },
157
+ {
158
+ titleKey: "timeline_obligation_risk_management",
159
+ articleRef: "Art. 9",
160
+ audiences: ["high-annex-iii"],
161
+ },
162
+ {
163
+ titleKey: "timeline_obligation_data_governance",
164
+ articleRef: "Art. 10",
165
+ audiences: ["high-annex-iii"],
166
+ },
167
+ {
168
+ titleKey: "timeline_obligation_transparency_deployers",
169
+ articleRef: "Art. 13",
170
+ audiences: ["high-annex-iii"],
171
+ },
172
+ {
173
+ titleKey: "timeline_obligation_human_oversight",
174
+ articleRef: "Art. 14",
175
+ audiences: ["high-annex-iii"],
176
+ },
177
+ {
178
+ titleKey: "timeline_obligation_conformity_assessment",
179
+ articleRef: "Art. 43",
180
+ audiences: ["high-annex-iii"],
181
+ },
182
+ {
183
+ titleKey: "timeline_obligation_ce_marking",
184
+ articleRef: "Art. 48",
185
+ audiences: ["high-annex-iii"],
186
+ },
187
+ {
188
+ titleKey: "timeline_obligation_eu_database",
189
+ articleRef: "Art. 49",
190
+ audiences: ["high-annex-iii"],
191
+ },
192
+ {
193
+ titleKey: "timeline_obligation_limited_risk_transparency",
194
+ articleRef: "Art. 50",
195
+ audiences: ["limited"],
196
+ maxPenalty: "€15M / 3%",
197
+ },
198
+ {
199
+ titleKey: "timeline_obligation_post_market",
200
+ articleRef: "Art. 72–73",
201
+ audiences: ["high-annex-iii"],
202
+ },
203
+ ],
204
+ },
205
+ {
206
+ id: "phase-4-high-risk-annex-i",
207
+ isoDate: "2027-08-02",
208
+ chapterRef: "Art. 113(4) – Art. 6(1) + Annex I",
209
+ titleKey: "timeline_phase4_title",
210
+ subtitleKey: "timeline_phase4_subtitle",
211
+ obligations: [
212
+ {
213
+ titleKey: "timeline_obligation_annex_i_products",
214
+ articleRef: "Art. 6(1) + Annex I",
215
+ audiences: ["high-annex-i"],
216
+ maxPenalty: "€15M / 3%",
217
+ },
218
+ {
219
+ titleKey: "timeline_obligation_gpai_grace_period_ends",
220
+ articleRef: "Art. 113(5)",
221
+ audiences: ["gpai"],
222
+ },
223
+ ],
224
+ },
225
+ ];
226
+
227
+ /**
228
+ * Digital Omnibus proposed changes (November 2025).
229
+ * NOT YET LAW – under negotiation by EP and Council as of early 2026.
230
+ */
231
+ export const DIGITAL_OMNIBUS_PROPOSALS: DeadlinePhase[] = [
232
+ {
233
+ id: "omnibus-high-risk-annex-iii",
234
+ isoDate: "2027-12-02",
235
+ chapterRef: "Digital Omnibus (proposed)",
236
+ titleKey: "timeline_omnibus_phase3_title",
237
+ subtitleKey: "timeline_omnibus_phase3_subtitle",
238
+ proposed: true,
239
+ obligations: [
240
+ {
241
+ titleKey: "timeline_omnibus_high_risk_delayed",
242
+ articleRef: "Annex III (proposed)",
243
+ audiences: ["high-annex-iii"],
244
+ proposed: true,
245
+ },
246
+ ],
247
+ },
248
+ {
249
+ id: "omnibus-high-risk-annex-i",
250
+ isoDate: "2028-08-02",
251
+ chapterRef: "Digital Omnibus (proposed)",
252
+ titleKey: "timeline_omnibus_phase4_title",
253
+ subtitleKey: "timeline_omnibus_phase4_subtitle",
254
+ proposed: true,
255
+ obligations: [
256
+ {
257
+ titleKey: "timeline_omnibus_annex_i_delayed",
258
+ articleRef: "Annex I (proposed)",
259
+ audiences: ["high-annex-i"],
260
+ proposed: true,
261
+ },
262
+ ],
263
+ },
264
+ ];
265
+
266
+ export function getAudiencesForRiskLevel(level: RiskLevel): AffectedAudience[] {
267
+ switch (level) {
268
+ case "unacceptable":
269
+ return ["all", "prohibited"];
270
+ case "high":
271
+ return ["all", "high-annex-iii", "high-annex-i"];
272
+ case "limited":
273
+ return ["all", "limited"];
274
+ case "minimal":
275
+ return ["all", "minimal"];
276
+ }
277
+ }
278
+
279
+ export function getNextDeadline(): DeadlinePhase | null {
280
+ const today = new Date().toISOString().slice(0, 10);
281
+ const upcoming = EU_AI_ACT_TIMELINE.filter((p) => p.isoDate > today).sort((a, b) =>
282
+ a.isoDate.localeCompare(b.isoDate)
283
+ );
284
+ return upcoming[0] ?? null;
285
+ }
286
+
287
+ export function getTimelineForRiskLevel(
288
+ riskLevel: RiskLevel
289
+ ): (DeadlinePhase & { status: DeadlineStatus })[] {
290
+ const audiences = getAudiencesForRiskLevel(riskLevel);
291
+ return EU_AI_ACT_TIMELINE.filter((phase) =>
292
+ phase.obligations.some((o) => o.audiences.some((a) => audiences.includes(a)))
293
+ ).map((phase) => ({
294
+ ...phase,
295
+ status: getDeadlineStatus(phase.isoDate),
296
+ }));
297
+ }
298
+
299
+ /**
300
+ * Filter timeline phases by explicit audience list.
301
+ * Use this when the caller has audiences that don't map to a RiskLevel
302
+ * (e.g., GPAI providers need ["all", "gpai"]).
303
+ */
304
+ export function getTimelineForAudiences(
305
+ audiences: AffectedAudience[]
306
+ ): (DeadlinePhase & { status: DeadlineStatus })[] {
307
+ return EU_AI_ACT_TIMELINE.filter((phase) =>
308
+ phase.obligations.some((o) => o.audiences.some((a) => audiences.includes(a)))
309
+ ).map((phase) => ({
310
+ ...phase,
311
+ status: getDeadlineStatus(phase.isoDate),
312
+ }));
313
+ }