@ansvar/eu-regulations-mcp 1.0.0 → 1.1.1
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/README.md +60 -22
- package/data/regulations.db +0 -0
- package/dist/database/sqlite-adapter.d.ts +2 -2
- package/dist/database/sqlite-adapter.d.ts.map +1 -1
- package/dist/database/sqlite-adapter.js.map +1 -1
- package/dist/http-server.js +27 -5
- package/dist/http-server.js.map +1 -1
- package/dist/index.js +27 -4
- package/dist/index.js.map +1 -1
- package/dist/tools/about.d.ts +40 -0
- package/dist/tools/about.d.ts.map +1 -0
- package/dist/tools/about.js +61 -0
- package/dist/tools/about.js.map +1 -0
- package/dist/tools/list.d.ts +7 -0
- package/dist/tools/list.d.ts.map +1 -1
- package/dist/tools/list.js +73 -8
- package/dist/tools/list.js.map +1 -1
- package/dist/tools/registry.d.ts +11 -1
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js +56 -4
- package/dist/tools/registry.js.map +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +17 -5
- package/dist/worker.js.map +1 -1
- package/package.json +6 -5
- package/scripts/add-cross-references.sql +0 -200
- package/scripts/analyze-survey-responses.ts +0 -285
- package/scripts/build-db.ts +0 -421
- package/scripts/bulk-reingest-all.ts +0 -331
- package/scripts/check-updates.ts +0 -294
- package/scripts/extract-eprivacy-recitals.ts +0 -98
- package/scripts/ingest-eurlex-browser.ts +0 -113
- package/scripts/ingest-eurlex.ts +0 -349
- package/scripts/ingest-unece.ts +0 -382
- package/scripts/migrate-postgres.ts +0 -445
- package/scripts/migrate-to-postgres.ts +0 -353
- package/scripts/reingest-all-with-recitals.sh +0 -81
- package/scripts/sync-versions.ts +0 -206
- package/scripts/test-cross-refs.js +0 -26
- package/scripts/test-postgres-adapter.ts +0 -146
- package/scripts/update-dora-rts-metadata.ts +0 -112
- package/src/database/postgres-adapter.ts +0 -84
- package/src/database/sqlite-adapter.ts +0 -44
- package/src/database/types.ts +0 -10
- package/src/http-server.ts +0 -149
- package/src/index.ts +0 -61
- package/src/middleware/rate-limit.ts +0 -104
- package/src/tools/applicability.ts +0 -167
- package/src/tools/article.ts +0 -81
- package/src/tools/compare.ts +0 -217
- package/src/tools/definitions.ts +0 -49
- package/src/tools/evidence.ts +0 -84
- package/src/tools/list.ts +0 -124
- package/src/tools/map.ts +0 -86
- package/src/tools/recital.ts +0 -60
- package/src/tools/registry.ts +0 -311
- package/src/tools/search.ts +0 -297
- package/src/worker.ts +0 -708
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
interface RateLimitRecord {
|
|
2
|
-
count: number;
|
|
3
|
-
resetAt: number;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export interface RateLimitInfo {
|
|
7
|
-
allowed: boolean;
|
|
8
|
-
remaining: number;
|
|
9
|
-
resetAt: number;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Fixed-window rate limiter for IP-based request throttling.
|
|
14
|
-
*
|
|
15
|
-
* Uses fixed time windows that reset at specific intervals. This is simpler
|
|
16
|
-
* than true sliding windows but allows burst traffic at window boundaries
|
|
17
|
-
* (e.g., 100 requests at 09:59:59 + 100 at 10:00:01 = 200 in 2 seconds).
|
|
18
|
-
*
|
|
19
|
-
* Trade-off accepted: Simplicity and performance over burst protection.
|
|
20
|
-
* Suitable for basic rate limiting where occasional bursts are acceptable.
|
|
21
|
-
*
|
|
22
|
-
* @example
|
|
23
|
-
* const limiter = new RateLimiter(100, 3600000); // 100 requests per hour
|
|
24
|
-
* if (limiter.checkLimit(clientIP)) {
|
|
25
|
-
* // Allow request
|
|
26
|
-
* } else {
|
|
27
|
-
* // Return 429 Too Many Requests
|
|
28
|
-
* }
|
|
29
|
-
*/
|
|
30
|
-
export class RateLimiter {
|
|
31
|
-
private records = new Map<string, RateLimitRecord>();
|
|
32
|
-
private readonly maxRequests: number;
|
|
33
|
-
private readonly windowMs: number;
|
|
34
|
-
|
|
35
|
-
constructor(maxRequests: number, windowMs: number) {
|
|
36
|
-
this.maxRequests = maxRequests;
|
|
37
|
-
this.windowMs = windowMs;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Check if a request from the given IP should be allowed
|
|
42
|
-
* @param ip Client IP address
|
|
43
|
-
* @returns true if allowed, false if rate limited
|
|
44
|
-
*/
|
|
45
|
-
checkLimit(ip: string): boolean {
|
|
46
|
-
return this.getRateLimitInfo(ip).allowed;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Get detailed rate limit information for an IP
|
|
51
|
-
* @param ip Client IP address
|
|
52
|
-
* @returns Rate limit status with remaining requests and reset time
|
|
53
|
-
*/
|
|
54
|
-
getRateLimitInfo(ip: string): RateLimitInfo {
|
|
55
|
-
const now = Date.now();
|
|
56
|
-
let record = this.records.get(ip);
|
|
57
|
-
|
|
58
|
-
// Clean up if window expired
|
|
59
|
-
if (record && now > record.resetAt) {
|
|
60
|
-
this.records.delete(ip);
|
|
61
|
-
record = undefined;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Initialize new record if needed
|
|
65
|
-
if (!record) {
|
|
66
|
-
record = {
|
|
67
|
-
count: 0,
|
|
68
|
-
resetAt: now + this.windowMs,
|
|
69
|
-
};
|
|
70
|
-
this.records.set(ip, record);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Check if over limit
|
|
74
|
-
if (record.count >= this.maxRequests) {
|
|
75
|
-
return {
|
|
76
|
-
allowed: false,
|
|
77
|
-
remaining: 0,
|
|
78
|
-
resetAt: record.resetAt,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Increment and allow
|
|
83
|
-
record.count++;
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
allowed: true,
|
|
87
|
-
remaining: this.maxRequests - record.count,
|
|
88
|
-
resetAt: record.resetAt,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Clean up old records to prevent memory leak
|
|
94
|
-
* Should be called periodically (e.g., every hour)
|
|
95
|
-
*/
|
|
96
|
-
cleanup(): void {
|
|
97
|
-
const now = Date.now();
|
|
98
|
-
for (const [ip, record] of this.records.entries()) {
|
|
99
|
-
if (now > record.resetAt) {
|
|
100
|
-
this.records.delete(ip);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import type { DatabaseAdapter } from '../database/types.js';
|
|
2
|
-
|
|
3
|
-
export type Sector =
|
|
4
|
-
| 'financial'
|
|
5
|
-
| 'healthcare'
|
|
6
|
-
| 'energy'
|
|
7
|
-
| 'transport'
|
|
8
|
-
| 'digital_infrastructure'
|
|
9
|
-
| 'public_administration'
|
|
10
|
-
| 'manufacturing'
|
|
11
|
-
| 'other';
|
|
12
|
-
|
|
13
|
-
export interface ApplicabilityInput {
|
|
14
|
-
sector: Sector;
|
|
15
|
-
subsector?: string;
|
|
16
|
-
member_state?: string;
|
|
17
|
-
size?: 'sme' | 'large';
|
|
18
|
-
detail_level?: 'summary' | 'requirements' | 'full';
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface ApplicableRegulation {
|
|
22
|
-
regulation: string;
|
|
23
|
-
confidence: 'definite' | 'likely' | 'possible';
|
|
24
|
-
basis: string | null;
|
|
25
|
-
notes: string | null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface RegulationSummary {
|
|
29
|
-
id: string;
|
|
30
|
-
full_name: string;
|
|
31
|
-
confidence: 'definite' | 'likely' | 'possible';
|
|
32
|
-
basis: string | null;
|
|
33
|
-
notes: string | null;
|
|
34
|
-
key_requirements?: string[];
|
|
35
|
-
priority_deadline?: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface ApplicabilityResult {
|
|
39
|
-
entity: ApplicabilityInput;
|
|
40
|
-
applicable_regulations: ApplicableRegulation[];
|
|
41
|
-
summary?: {
|
|
42
|
-
total_count: number;
|
|
43
|
-
by_confidence: {
|
|
44
|
-
definite: number;
|
|
45
|
-
likely: number;
|
|
46
|
-
possible: number;
|
|
47
|
-
};
|
|
48
|
-
regulations_summary: RegulationSummary[];
|
|
49
|
-
next_steps?: string;
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export async function checkApplicability(
|
|
54
|
-
db: DatabaseAdapter,
|
|
55
|
-
input: ApplicabilityInput
|
|
56
|
-
): Promise<ApplicabilityResult> {
|
|
57
|
-
const { sector, subsector, detail_level = 'full' } = input;
|
|
58
|
-
|
|
59
|
-
// Query for matching rules - check both sector match and subsector match
|
|
60
|
-
// Note: We handle deduplication in JavaScript, so no need for DISTINCT ON
|
|
61
|
-
let sql = `
|
|
62
|
-
SELECT
|
|
63
|
-
regulation,
|
|
64
|
-
confidence,
|
|
65
|
-
basis_article as basis,
|
|
66
|
-
notes,
|
|
67
|
-
CASE confidence
|
|
68
|
-
WHEN 'definite' THEN 1
|
|
69
|
-
WHEN 'likely' THEN 2
|
|
70
|
-
WHEN 'possible' THEN 3
|
|
71
|
-
END as conf_order
|
|
72
|
-
FROM applicability_rules
|
|
73
|
-
WHERE applies = true
|
|
74
|
-
AND (
|
|
75
|
-
(sector = $1 AND (subsector IS NULL OR subsector = $2))
|
|
76
|
-
OR (sector = $3 AND subsector IS NULL)
|
|
77
|
-
)
|
|
78
|
-
ORDER BY regulation, conf_order
|
|
79
|
-
`;
|
|
80
|
-
|
|
81
|
-
const result = await db.query(sql, [sector, subsector || '', sector]);
|
|
82
|
-
|
|
83
|
-
const rows = result.rows as Array<{
|
|
84
|
-
regulation: string;
|
|
85
|
-
confidence: 'definite' | 'likely' | 'possible';
|
|
86
|
-
basis: string | null;
|
|
87
|
-
notes: string | null;
|
|
88
|
-
}>;
|
|
89
|
-
|
|
90
|
-
// Deduplicate by regulation, keeping highest confidence
|
|
91
|
-
const regulationMap = new Map<string, ApplicableRegulation>();
|
|
92
|
-
for (const row of rows) {
|
|
93
|
-
if (!regulationMap.has(row.regulation)) {
|
|
94
|
-
regulationMap.set(row.regulation, {
|
|
95
|
-
regulation: row.regulation,
|
|
96
|
-
confidence: row.confidence,
|
|
97
|
-
basis: row.basis,
|
|
98
|
-
notes: row.notes,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const applicable_regulations = Array.from(regulationMap.values());
|
|
104
|
-
|
|
105
|
-
// If summary detail level requested, add summary section
|
|
106
|
-
let summary;
|
|
107
|
-
if (detail_level === 'summary') {
|
|
108
|
-
// Get regulation metadata for summary
|
|
109
|
-
const regIds = applicable_regulations.map(r => r.regulation);
|
|
110
|
-
const placeholders = regIds.map((_, i) => `$${i + 1}`).join(', ');
|
|
111
|
-
const regDataResult = await db.query(
|
|
112
|
-
`SELECT id, full_name, effective_date
|
|
113
|
-
FROM regulations
|
|
114
|
-
WHERE id IN (${placeholders})`,
|
|
115
|
-
regIds
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
const regData = regDataResult.rows as Array<{
|
|
119
|
-
id: string;
|
|
120
|
-
full_name: string;
|
|
121
|
-
effective_date: string | null;
|
|
122
|
-
}>;
|
|
123
|
-
|
|
124
|
-
const regMetadata = new Map(regData.map(r => [r.id, r]));
|
|
125
|
-
|
|
126
|
-
// Priority deadlines for key regulations
|
|
127
|
-
const priorityDeadlines: Record<string, string> = {
|
|
128
|
-
'DORA': 'Jan 17, 2025 (ACTIVE)',
|
|
129
|
-
'NIS2': 'Oct 17, 2024 (Swedish implementation)',
|
|
130
|
-
'AI_ACT': 'Aug 2, 2026 (high-risk systems)',
|
|
131
|
-
'EIDAS2': 'Late 2027 (wallet acceptance)',
|
|
132
|
-
'CSRD': 'Phased 2025-2028',
|
|
133
|
-
'CSDDD': 'Implementation roadmap needed',
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
const regulations_summary: RegulationSummary[] = applicable_regulations.map(reg => {
|
|
137
|
-
const metadata = regMetadata.get(reg.regulation);
|
|
138
|
-
return {
|
|
139
|
-
id: reg.regulation,
|
|
140
|
-
full_name: metadata?.full_name || reg.regulation,
|
|
141
|
-
confidence: reg.confidence,
|
|
142
|
-
basis: reg.basis,
|
|
143
|
-
notes: reg.notes,
|
|
144
|
-
priority_deadline: priorityDeadlines[reg.regulation],
|
|
145
|
-
};
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
const by_confidence = {
|
|
149
|
-
definite: applicable_regulations.filter(r => r.confidence === 'definite').length,
|
|
150
|
-
likely: applicable_regulations.filter(r => r.confidence === 'likely').length,
|
|
151
|
-
possible: applicable_regulations.filter(r => r.confidence === 'possible').length,
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
summary = {
|
|
155
|
-
total_count: applicable_regulations.length,
|
|
156
|
-
by_confidence,
|
|
157
|
-
regulations_summary,
|
|
158
|
-
next_steps: "For detailed requirements, use detail_level='requirements'. For full article-level detail, use detail_level='full'.",
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return {
|
|
163
|
-
entity: input,
|
|
164
|
-
applicable_regulations,
|
|
165
|
-
summary,
|
|
166
|
-
};
|
|
167
|
-
}
|
package/src/tools/article.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import type { DatabaseAdapter } from '../database/types.js';
|
|
2
|
-
|
|
3
|
-
export interface GetArticleInput {
|
|
4
|
-
regulation: string;
|
|
5
|
-
article: string;
|
|
6
|
-
include_recitals?: boolean;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface Article {
|
|
10
|
-
regulation: string;
|
|
11
|
-
article_number: string;
|
|
12
|
-
title: string | null;
|
|
13
|
-
text: string;
|
|
14
|
-
chapter: string | null;
|
|
15
|
-
recitals: string[] | null;
|
|
16
|
-
cross_references: string[] | null;
|
|
17
|
-
truncated?: boolean;
|
|
18
|
-
original_length?: number;
|
|
19
|
-
token_estimate?: number;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export async function getArticle(
|
|
23
|
-
db: DatabaseAdapter,
|
|
24
|
-
input: GetArticleInput
|
|
25
|
-
): Promise<Article | null> {
|
|
26
|
-
const { regulation, article } = input;
|
|
27
|
-
|
|
28
|
-
const sql = `
|
|
29
|
-
SELECT
|
|
30
|
-
regulation,
|
|
31
|
-
article_number,
|
|
32
|
-
title,
|
|
33
|
-
text,
|
|
34
|
-
chapter,
|
|
35
|
-
recitals,
|
|
36
|
-
cross_references
|
|
37
|
-
FROM articles
|
|
38
|
-
WHERE regulation = $1 AND article_number = $2
|
|
39
|
-
`;
|
|
40
|
-
|
|
41
|
-
const result = await db.query(sql, [regulation, article]);
|
|
42
|
-
|
|
43
|
-
if (result.rows.length === 0) {
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const row = result.rows[0] as {
|
|
48
|
-
regulation: string;
|
|
49
|
-
article_number: string;
|
|
50
|
-
title: string | null;
|
|
51
|
-
text: string;
|
|
52
|
-
chapter: string | null;
|
|
53
|
-
recitals: string | null;
|
|
54
|
-
cross_references: string | null;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
// Token management: Truncate very large articles to prevent context overflow
|
|
58
|
-
const MAX_CHARS = 50000; // ~12,500 tokens (safe for 200k context window)
|
|
59
|
-
const originalLength = row.text.length;
|
|
60
|
-
const tokenEstimate = Math.ceil(originalLength / 4); // ~4 chars per token
|
|
61
|
-
let text = row.text;
|
|
62
|
-
let truncated = false;
|
|
63
|
-
|
|
64
|
-
if (originalLength > MAX_CHARS) {
|
|
65
|
-
text = row.text.substring(0, MAX_CHARS) + '\n\n[... Article truncated due to length. Original: ' + originalLength + ' chars (~' + tokenEstimate + ' tokens). Use search_regulations to find specific sections.]';
|
|
66
|
-
truncated = true;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return {
|
|
70
|
-
regulation: row.regulation,
|
|
71
|
-
article_number: row.article_number,
|
|
72
|
-
title: row.title,
|
|
73
|
-
text,
|
|
74
|
-
chapter: row.chapter,
|
|
75
|
-
recitals: row.recitals ? JSON.parse(row.recitals) : null,
|
|
76
|
-
cross_references: row.cross_references ? JSON.parse(row.cross_references) : null,
|
|
77
|
-
truncated,
|
|
78
|
-
original_length: truncated ? originalLength : undefined,
|
|
79
|
-
token_estimate: truncated ? tokenEstimate : undefined,
|
|
80
|
-
};
|
|
81
|
-
}
|
package/src/tools/compare.ts
DELETED
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
import type { DatabaseAdapter } from '../database/types.js';
|
|
2
|
-
import { searchRegulations } from './search.js';
|
|
3
|
-
|
|
4
|
-
export interface CompareInput {
|
|
5
|
-
topic: string;
|
|
6
|
-
regulations: string[];
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface RegulationComparison {
|
|
10
|
-
regulation: string;
|
|
11
|
-
requirements: string[];
|
|
12
|
-
articles: string[];
|
|
13
|
-
timelines?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface CompareResult {
|
|
17
|
-
topic: string;
|
|
18
|
-
regulations: RegulationComparison[];
|
|
19
|
-
key_differences?: string[];
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Concept synonym families for cross-regulation terminology matching.
|
|
24
|
-
* Each key is a canonical concept; values are alternative terms used across
|
|
25
|
-
* different EU regulations for the same underlying requirement.
|
|
26
|
-
*/
|
|
27
|
-
const CONCEPT_SYNONYMS: Record<string, string[]> = {
|
|
28
|
-
// Incident & breach reporting
|
|
29
|
-
'incident reporting': ['breach notification', 'incident management', 'incident report', 'significant incident', 'security incident'],
|
|
30
|
-
'breach notification': ['incident reporting', 'data breach', 'personal data breach', 'incident notification', 'security breach'],
|
|
31
|
-
|
|
32
|
-
// Data protection & privacy
|
|
33
|
-
'data protection': ['privacy', 'personal data', 'data processing', 'data subject rights', 'information protection'],
|
|
34
|
-
'privacy': ['data protection', 'personal data', 'confidentiality', 'private life', 'ePrivacy'],
|
|
35
|
-
|
|
36
|
-
// Access control & authentication
|
|
37
|
-
'access control': ['authentication', 'identity verification', 'authorisation', 'identity management', 'strong authentication'],
|
|
38
|
-
'authentication': ['access control', 'identity verification', 'electronic identification', 'multi-factor', 'strong user authentication'],
|
|
39
|
-
|
|
40
|
-
// Risk management & assessment
|
|
41
|
-
'risk management': ['risk assessment', 'risk analysis', 'risk evaluation', 'threat assessment', 'ICT risk'],
|
|
42
|
-
'risk assessment': ['risk management', 'risk analysis', 'impact assessment', 'threat analysis', 'vulnerability assessment'],
|
|
43
|
-
|
|
44
|
-
// Encryption & cryptography
|
|
45
|
-
'encryption': ['cryptography', 'cryptographic', 'cipher', 'pseudonymisation', 'data at rest'],
|
|
46
|
-
'cryptography': ['encryption', 'cryptographic controls', 'cipher', 'key management', 'digital signature'],
|
|
47
|
-
|
|
48
|
-
// Supply chain & third-party
|
|
49
|
-
'supply chain': ['third-party', 'third party', 'ICT services', 'outsourcing', 'subcontracting', 'vendor'],
|
|
50
|
-
'third-party': ['supply chain', 'third party', 'ICT third-party', 'service provider', 'outsourcing', 'subcontractor'],
|
|
51
|
-
|
|
52
|
-
// Business continuity & disaster recovery
|
|
53
|
-
'business continuity': ['disaster recovery', 'continuity plan', 'operational resilience', 'recovery', 'backup'],
|
|
54
|
-
'disaster recovery': ['business continuity', 'continuity plan', 'restoration', 'backup', 'recovery objective'],
|
|
55
|
-
|
|
56
|
-
// Vulnerability management & disclosure
|
|
57
|
-
'vulnerability management': ['vulnerability disclosure', 'vulnerability handling', 'security flaw', 'patch management', 'security update'],
|
|
58
|
-
'vulnerability disclosure': ['vulnerability management', 'coordinated disclosure', 'security vulnerability', 'responsible disclosure'],
|
|
59
|
-
|
|
60
|
-
// Audit & compliance & certification
|
|
61
|
-
'audit': ['compliance', 'certification', 'conformity assessment', 'supervisory', 'inspection', 'assurance'],
|
|
62
|
-
'compliance': ['audit', 'certification', 'regulatory', 'supervisory authority', 'conformity', 'enforcement'],
|
|
63
|
-
'certification': ['audit', 'compliance', 'conformity assessment', 'accreditation', 'qualified status', 'cybersecurity certification'],
|
|
64
|
-
|
|
65
|
-
// Transparency & reporting
|
|
66
|
-
'transparency': ['reporting', 'disclosure', 'information provision', 'public reporting', 'register'],
|
|
67
|
-
'reporting': ['transparency', 'disclosure', 'notification', 'documentation', 'reporting obligation'],
|
|
68
|
-
|
|
69
|
-
// Governance & accountability
|
|
70
|
-
'governance': ['accountability', 'management body', 'board responsibility', 'oversight', 'organisational structure'],
|
|
71
|
-
'accountability': ['governance', 'responsibility', 'management body', 'data controller', 'duty of care'],
|
|
72
|
-
|
|
73
|
-
// Penetration testing & security testing
|
|
74
|
-
'penetration testing': ['security testing', 'TLPT', 'threat-led', 'red team', 'vulnerability testing'],
|
|
75
|
-
'security testing': ['penetration testing', 'resilience testing', 'TLPT', 'vulnerability assessment', 'operational testing'],
|
|
76
|
-
|
|
77
|
-
// Consent & lawful basis
|
|
78
|
-
'consent': ['lawful basis', 'legal basis', 'legitimate interest', 'data subject consent', 'explicit consent'],
|
|
79
|
-
'lawful basis': ['consent', 'legal basis', 'legitimate interest', 'contractual necessity', 'legal obligation'],
|
|
80
|
-
|
|
81
|
-
// Data portability & interoperability
|
|
82
|
-
'data portability': ['interoperability', 'data transfer', 'data migration', 'portability right', 'data access'],
|
|
83
|
-
'interoperability': ['data portability', 'compatibility', 'standardisation', 'cross-border', 'mutual recognition'],
|
|
84
|
-
|
|
85
|
-
// Record keeping & documentation
|
|
86
|
-
'record keeping': ['documentation', 'register', 'records of processing', 'logging', 'traceability'],
|
|
87
|
-
'documentation': ['record keeping', 'register', 'records', 'evidence', 'logging', 'information register'],
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Find synonym terms for a given topic query.
|
|
92
|
-
* Returns the original topic plus up to 4 synonym terms.
|
|
93
|
-
*/
|
|
94
|
-
function getSynonyms(topic: string): string[] {
|
|
95
|
-
const lowerTopic = topic.toLowerCase();
|
|
96
|
-
const synonyms = new Set<string>();
|
|
97
|
-
|
|
98
|
-
for (const [concept, terms] of Object.entries(CONCEPT_SYNONYMS)) {
|
|
99
|
-
// Check if the topic matches or contains a concept key
|
|
100
|
-
if (lowerTopic.includes(concept) || concept.includes(lowerTopic)) {
|
|
101
|
-
for (const term of terms) {
|
|
102
|
-
synonyms.add(term);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
// Check if the topic matches any synonym term
|
|
106
|
-
for (const term of terms) {
|
|
107
|
-
if (lowerTopic.includes(term) || term.includes(lowerTopic)) {
|
|
108
|
-
synonyms.add(concept);
|
|
109
|
-
for (const t of terms) {
|
|
110
|
-
synonyms.add(t);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Remove the original topic itself and limit to 4 synonyms
|
|
117
|
-
synonyms.delete(lowerTopic);
|
|
118
|
-
return Array.from(synonyms).slice(0, 4);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Extract timeline mentions from text (e.g., "24 hours", "72 hours")
|
|
123
|
-
*/
|
|
124
|
-
function extractTimelines(text: string): string | undefined {
|
|
125
|
-
const timelinePatterns = [
|
|
126
|
-
/(\d+)\s*hours?/gi,
|
|
127
|
-
/(\d+)\s*days?/gi,
|
|
128
|
-
/without\s+undue\s+delay/gi,
|
|
129
|
-
/immediately/gi,
|
|
130
|
-
];
|
|
131
|
-
|
|
132
|
-
const matches: string[] = [];
|
|
133
|
-
for (const pattern of timelinePatterns) {
|
|
134
|
-
const found = text.match(pattern);
|
|
135
|
-
if (found) {
|
|
136
|
-
matches.push(...found);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return matches.length > 0 ? matches.join(', ') : undefined;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export async function compareRequirements(
|
|
144
|
-
db: DatabaseAdapter,
|
|
145
|
-
input: CompareInput
|
|
146
|
-
): Promise<CompareResult> {
|
|
147
|
-
const { topic, regulations } = input;
|
|
148
|
-
|
|
149
|
-
// Get synonym terms for expanded search
|
|
150
|
-
const synonyms = getSynonyms(topic);
|
|
151
|
-
const searchTerms = [topic, ...synonyms];
|
|
152
|
-
|
|
153
|
-
const comparisons: RegulationComparison[] = [];
|
|
154
|
-
|
|
155
|
-
for (const regulation of regulations) {
|
|
156
|
-
// Search with original topic + synonym terms, then merge results
|
|
157
|
-
const allResults: Map<string, { article: string; snippet: string; relevance: number }> = new Map();
|
|
158
|
-
|
|
159
|
-
for (const term of searchTerms) {
|
|
160
|
-
const results = await searchRegulations(db, {
|
|
161
|
-
query: term,
|
|
162
|
-
regulations: [regulation],
|
|
163
|
-
limit: 5,
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
for (const result of results) {
|
|
167
|
-
const existing = allResults.get(result.article);
|
|
168
|
-
if (!existing || result.relevance > existing.relevance) {
|
|
169
|
-
allResults.set(result.article, {
|
|
170
|
-
article: result.article,
|
|
171
|
-
snippet: result.snippet,
|
|
172
|
-
relevance: result.relevance,
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Sort by relevance and take top 5
|
|
179
|
-
const mergedResults = Array.from(allResults.values())
|
|
180
|
-
.sort((a, b) => b.relevance - a.relevance)
|
|
181
|
-
.slice(0, 5);
|
|
182
|
-
|
|
183
|
-
// Get full article text for timeline extraction
|
|
184
|
-
const articles: string[] = [];
|
|
185
|
-
const requirements: string[] = [];
|
|
186
|
-
let combinedText = '';
|
|
187
|
-
|
|
188
|
-
for (const result of mergedResults) {
|
|
189
|
-
articles.push(result.article);
|
|
190
|
-
requirements.push(result.snippet.replace(/>>>/g, '').replace(/<<</g, ''));
|
|
191
|
-
|
|
192
|
-
// Get full text for timeline extraction
|
|
193
|
-
const fullArticleResult = await db.query(
|
|
194
|
-
`SELECT text FROM articles WHERE regulation = $1 AND article_number = $2`,
|
|
195
|
-
[regulation, result.article]
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
if (fullArticleResult.rows.length > 0) {
|
|
199
|
-
combinedText += ' ' + (fullArticleResult.rows[0] as { text: string }).text;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const timelines = extractTimelines(combinedText);
|
|
204
|
-
|
|
205
|
-
comparisons.push({
|
|
206
|
-
regulation,
|
|
207
|
-
requirements,
|
|
208
|
-
articles,
|
|
209
|
-
timelines,
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return {
|
|
214
|
-
topic,
|
|
215
|
-
regulations: comparisons,
|
|
216
|
-
};
|
|
217
|
-
}
|
package/src/tools/definitions.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import type { DatabaseAdapter } from '../database/types.js';
|
|
2
|
-
|
|
3
|
-
export interface DefinitionsInput {
|
|
4
|
-
term: string;
|
|
5
|
-
regulation?: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface Definition {
|
|
9
|
-
term: string;
|
|
10
|
-
regulation: string;
|
|
11
|
-
article: string;
|
|
12
|
-
definition: string;
|
|
13
|
-
related_terms?: string[];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export async function getDefinitions(
|
|
17
|
-
db: DatabaseAdapter,
|
|
18
|
-
input: DefinitionsInput
|
|
19
|
-
): Promise<Definition[]> {
|
|
20
|
-
const { term, regulation } = input;
|
|
21
|
-
|
|
22
|
-
let sql = `
|
|
23
|
-
SELECT
|
|
24
|
-
term,
|
|
25
|
-
regulation,
|
|
26
|
-
article,
|
|
27
|
-
definition
|
|
28
|
-
FROM definitions
|
|
29
|
-
WHERE term ILIKE $1
|
|
30
|
-
`;
|
|
31
|
-
|
|
32
|
-
const params: string[] = [`%${term}%`];
|
|
33
|
-
|
|
34
|
-
if (regulation) {
|
|
35
|
-
sql += ` AND regulation = $2`;
|
|
36
|
-
params.push(regulation);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
sql += ` ORDER BY regulation, term`;
|
|
40
|
-
|
|
41
|
-
const result = await db.query(sql, params);
|
|
42
|
-
|
|
43
|
-
return result.rows.map((row: any) => ({
|
|
44
|
-
term: row.term,
|
|
45
|
-
regulation: row.regulation,
|
|
46
|
-
article: row.article,
|
|
47
|
-
definition: row.definition,
|
|
48
|
-
}));
|
|
49
|
-
}
|
package/src/tools/evidence.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import type { DatabaseAdapter } from '../database/types.js';
|
|
2
|
-
|
|
3
|
-
export interface EvidenceInput {
|
|
4
|
-
regulation?: string;
|
|
5
|
-
article?: string;
|
|
6
|
-
evidence_type?: 'document' | 'log' | 'test_result' | 'certification' | 'policy' | 'procedure';
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface EvidenceRequirement {
|
|
10
|
-
regulation: string;
|
|
11
|
-
article: string;
|
|
12
|
-
requirement_summary: string;
|
|
13
|
-
evidence_type: string;
|
|
14
|
-
artifact_name: string;
|
|
15
|
-
artifact_example: string | null;
|
|
16
|
-
description: string | null;
|
|
17
|
-
retention_period: string | null;
|
|
18
|
-
auditor_questions: string[];
|
|
19
|
-
maturity_levels: {
|
|
20
|
-
basic?: string;
|
|
21
|
-
intermediate?: string;
|
|
22
|
-
advanced?: string;
|
|
23
|
-
} | null;
|
|
24
|
-
cross_references: string[];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export async function getEvidenceRequirements(
|
|
28
|
-
db: DatabaseAdapter,
|
|
29
|
-
input: EvidenceInput
|
|
30
|
-
): Promise<EvidenceRequirement[]> {
|
|
31
|
-
const { regulation, article, evidence_type } = input;
|
|
32
|
-
|
|
33
|
-
let sql = `
|
|
34
|
-
SELECT
|
|
35
|
-
regulation,
|
|
36
|
-
article,
|
|
37
|
-
requirement_summary,
|
|
38
|
-
evidence_type,
|
|
39
|
-
artifact_name,
|
|
40
|
-
artifact_example,
|
|
41
|
-
description,
|
|
42
|
-
retention_period,
|
|
43
|
-
auditor_questions,
|
|
44
|
-
maturity_levels,
|
|
45
|
-
cross_references
|
|
46
|
-
FROM evidence_requirements
|
|
47
|
-
WHERE 1=1
|
|
48
|
-
`;
|
|
49
|
-
|
|
50
|
-
const params: string[] = [];
|
|
51
|
-
|
|
52
|
-
if (regulation) {
|
|
53
|
-
sql += ` AND regulation = $${params.length + 1}`;
|
|
54
|
-
params.push(regulation);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (article) {
|
|
58
|
-
sql += ` AND article = $${params.length + 1}`;
|
|
59
|
-
params.push(article);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (evidence_type) {
|
|
63
|
-
sql += ` AND evidence_type = $${params.length + 1}`;
|
|
64
|
-
params.push(evidence_type);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
sql += ` ORDER BY regulation, article::INTEGER, evidence_type`;
|
|
68
|
-
|
|
69
|
-
const result = await db.query(sql, params);
|
|
70
|
-
|
|
71
|
-
return result.rows.map((row: any) => ({
|
|
72
|
-
regulation: row.regulation,
|
|
73
|
-
article: row.article,
|
|
74
|
-
requirement_summary: row.requirement_summary,
|
|
75
|
-
evidence_type: row.evidence_type,
|
|
76
|
-
artifact_name: row.artifact_name,
|
|
77
|
-
artifact_example: row.artifact_example,
|
|
78
|
-
description: row.description,
|
|
79
|
-
retention_period: row.retention_period,
|
|
80
|
-
auditor_questions: row.auditor_questions ? JSON.parse(row.auditor_questions) : [],
|
|
81
|
-
maturity_levels: row.maturity_levels ? JSON.parse(row.maturity_levels) : null,
|
|
82
|
-
cross_references: row.cross_references ? JSON.parse(row.cross_references) : [],
|
|
83
|
-
}));
|
|
84
|
-
}
|