@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.
Files changed (58) hide show
  1. package/README.md +60 -22
  2. package/data/regulations.db +0 -0
  3. package/dist/database/sqlite-adapter.d.ts +2 -2
  4. package/dist/database/sqlite-adapter.d.ts.map +1 -1
  5. package/dist/database/sqlite-adapter.js.map +1 -1
  6. package/dist/http-server.js +27 -5
  7. package/dist/http-server.js.map +1 -1
  8. package/dist/index.js +27 -4
  9. package/dist/index.js.map +1 -1
  10. package/dist/tools/about.d.ts +40 -0
  11. package/dist/tools/about.d.ts.map +1 -0
  12. package/dist/tools/about.js +61 -0
  13. package/dist/tools/about.js.map +1 -0
  14. package/dist/tools/list.d.ts +7 -0
  15. package/dist/tools/list.d.ts.map +1 -1
  16. package/dist/tools/list.js +73 -8
  17. package/dist/tools/list.js.map +1 -1
  18. package/dist/tools/registry.d.ts +11 -1
  19. package/dist/tools/registry.d.ts.map +1 -1
  20. package/dist/tools/registry.js +56 -4
  21. package/dist/tools/registry.js.map +1 -1
  22. package/dist/worker.d.ts.map +1 -1
  23. package/dist/worker.js +17 -5
  24. package/dist/worker.js.map +1 -1
  25. package/package.json +6 -5
  26. package/scripts/add-cross-references.sql +0 -200
  27. package/scripts/analyze-survey-responses.ts +0 -285
  28. package/scripts/build-db.ts +0 -421
  29. package/scripts/bulk-reingest-all.ts +0 -331
  30. package/scripts/check-updates.ts +0 -294
  31. package/scripts/extract-eprivacy-recitals.ts +0 -98
  32. package/scripts/ingest-eurlex-browser.ts +0 -113
  33. package/scripts/ingest-eurlex.ts +0 -349
  34. package/scripts/ingest-unece.ts +0 -382
  35. package/scripts/migrate-postgres.ts +0 -445
  36. package/scripts/migrate-to-postgres.ts +0 -353
  37. package/scripts/reingest-all-with-recitals.sh +0 -81
  38. package/scripts/sync-versions.ts +0 -206
  39. package/scripts/test-cross-refs.js +0 -26
  40. package/scripts/test-postgres-adapter.ts +0 -146
  41. package/scripts/update-dora-rts-metadata.ts +0 -112
  42. package/src/database/postgres-adapter.ts +0 -84
  43. package/src/database/sqlite-adapter.ts +0 -44
  44. package/src/database/types.ts +0 -10
  45. package/src/http-server.ts +0 -149
  46. package/src/index.ts +0 -61
  47. package/src/middleware/rate-limit.ts +0 -104
  48. package/src/tools/applicability.ts +0 -167
  49. package/src/tools/article.ts +0 -81
  50. package/src/tools/compare.ts +0 -217
  51. package/src/tools/definitions.ts +0 -49
  52. package/src/tools/evidence.ts +0 -84
  53. package/src/tools/list.ts +0 -124
  54. package/src/tools/map.ts +0 -86
  55. package/src/tools/recital.ts +0 -60
  56. package/src/tools/registry.ts +0 -311
  57. package/src/tools/search.ts +0 -297
  58. 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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }