@ansvar/eu-regulations-mcp 0.8.0 → 1.1.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/README.md +76 -29
- package/data/regulations.db +0 -0
- package/data/seed/applicability/chips-act.json +67 -0
- package/data/seed/applicability/crma.json +85 -0
- package/data/seed/chips-act.json +714 -0
- package/data/seed/crma.json +877 -0
- package/data/seed/mappings/iso27001-chips-act.json +50 -0
- package/data/seed/mappings/iso27001-crma.json +50 -0
- package/data/seed/mappings/nist-csf-chips-act.json +56 -0
- package/data/seed/mappings/nist-csf-crma.json +56 -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 +8 -7
- 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 -346
- 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,285 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env tsx
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Survey Response Analysis Helper
|
|
5
|
-
*
|
|
6
|
-
* Helps analyze delegated acts survey responses from GitHub Discussions.
|
|
7
|
-
* Can be run manually or integrated into workflow.
|
|
8
|
-
*
|
|
9
|
-
* Usage:
|
|
10
|
-
* npx tsx scripts/analyze-survey-responses.ts [discussion-number]
|
|
11
|
-
*
|
|
12
|
-
* Requires:
|
|
13
|
-
* GITHUB_TOKEN environment variable (for API access)
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
interface SurveyResponse {
|
|
17
|
-
id: string;
|
|
18
|
-
author: string;
|
|
19
|
-
createdAt: string;
|
|
20
|
-
urgency: string;
|
|
21
|
-
regulations: string[];
|
|
22
|
-
useCase: string;
|
|
23
|
-
specificStandards: string;
|
|
24
|
-
currentWorkaround: string;
|
|
25
|
-
willingToHelp: string[];
|
|
26
|
-
additionalContext: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
interface AnalysisResults {
|
|
30
|
-
totalResponses: number;
|
|
31
|
-
urgencyBreakdown: Record<string, number>;
|
|
32
|
-
topRegulations: Record<string, number>;
|
|
33
|
-
betaTesters: number;
|
|
34
|
-
potentialSponsors: number;
|
|
35
|
-
qualityScore: number;
|
|
36
|
-
recommendation: 'proceed' | 'phased' | 'defer';
|
|
37
|
-
reasoning: string[];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const URGENCY_LEVELS = {
|
|
41
|
-
blocking: '🔴 Blocking current work',
|
|
42
|
-
threeMonth: '🟡 Needed within 3 months',
|
|
43
|
-
sixMonth: '🟢 Needed within 6 months',
|
|
44
|
-
niceToHave: '🔵 Nice to have eventually',
|
|
45
|
-
notNeeded: '⚪ Not needed',
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const DECISION_THRESHOLDS = {
|
|
49
|
-
proceed: {
|
|
50
|
-
minResponses: 20,
|
|
51
|
-
minHighUrgency: 0.5,
|
|
52
|
-
minBetaTesters: 3,
|
|
53
|
-
},
|
|
54
|
-
phased: {
|
|
55
|
-
minResponses: 10,
|
|
56
|
-
minMediumUrgency: 0.3,
|
|
57
|
-
minBetaTesters: 1,
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Parse a survey response from GitHub Discussion comment body
|
|
63
|
-
*/
|
|
64
|
-
function parseResponse(commentBody: string, author: string, createdAt: string): SurveyResponse | null {
|
|
65
|
-
try {
|
|
66
|
-
// This is a simplified parser - actual implementation would parse the structured form data
|
|
67
|
-
// from GitHub Discussion form responses
|
|
68
|
-
|
|
69
|
-
const urgencyMatch = commentBody.match(/urgency[:\s]+(.+)/i);
|
|
70
|
-
const regulationsMatch = commentBody.match(/regulations[:\s]+(.+)/i);
|
|
71
|
-
const useCaseMatch = commentBody.match(/use case[:\s]+(.+)/i);
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
id: `response-${Date.now()}`,
|
|
75
|
-
author,
|
|
76
|
-
createdAt,
|
|
77
|
-
urgency: urgencyMatch?.[1] || 'unknown',
|
|
78
|
-
regulations: regulationsMatch?.[1]?.split(',').map(r => r.trim()) || [],
|
|
79
|
-
useCase: useCaseMatch?.[1] || '',
|
|
80
|
-
specificStandards: '',
|
|
81
|
-
currentWorkaround: '',
|
|
82
|
-
willingToHelp: [],
|
|
83
|
-
additionalContext: '',
|
|
84
|
-
};
|
|
85
|
-
} catch (error) {
|
|
86
|
-
console.error('Failed to parse response:', error);
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Analyze collected survey responses
|
|
93
|
-
*/
|
|
94
|
-
function analyzeResponses(responses: SurveyResponse[]): AnalysisResults {
|
|
95
|
-
const urgencyBreakdown: Record<string, number> = {};
|
|
96
|
-
const topRegulations: Record<string, number> = {};
|
|
97
|
-
let betaTesters = 0;
|
|
98
|
-
let potentialSponsors = 0;
|
|
99
|
-
let qualityScore = 0;
|
|
100
|
-
|
|
101
|
-
for (const response of responses) {
|
|
102
|
-
// Count urgency levels
|
|
103
|
-
urgencyBreakdown[response.urgency] = (urgencyBreakdown[response.urgency] || 0) + 1;
|
|
104
|
-
|
|
105
|
-
// Count regulation requests
|
|
106
|
-
for (const reg of response.regulations) {
|
|
107
|
-
topRegulations[reg] = (topRegulations[reg] || 0) + 1;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Count beta testers
|
|
111
|
-
if (response.willingToHelp.includes('beta test')) {
|
|
112
|
-
betaTesters++;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Count potential sponsors
|
|
116
|
-
if (response.willingToHelp.includes('sponsoring')) {
|
|
117
|
-
potentialSponsors++;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Calculate quality score (0-100)
|
|
121
|
-
let responseQuality = 0;
|
|
122
|
-
if (response.useCase.length > 100) responseQuality += 30;
|
|
123
|
-
if (response.specificStandards.length > 0) responseQuality += 30;
|
|
124
|
-
if (response.regulations.length > 0) responseQuality += 20;
|
|
125
|
-
if (response.additionalContext.length > 50) responseQuality += 20;
|
|
126
|
-
qualityScore += responseQuality;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
qualityScore = responses.length > 0 ? qualityScore / responses.length : 0;
|
|
130
|
-
|
|
131
|
-
// Determine recommendation
|
|
132
|
-
const highUrgency = (urgencyBreakdown['🔴 Blocking current work'] || 0) +
|
|
133
|
-
(urgencyBreakdown['🟡 Needed within 3 months'] || 0);
|
|
134
|
-
const highUrgencyPercent = responses.length > 0 ? highUrgency / responses.length : 0;
|
|
135
|
-
|
|
136
|
-
let recommendation: 'proceed' | 'phased' | 'defer' = 'defer';
|
|
137
|
-
const reasoning: string[] = [];
|
|
138
|
-
|
|
139
|
-
if (responses.length >= DECISION_THRESHOLDS.proceed.minResponses &&
|
|
140
|
-
highUrgencyPercent >= DECISION_THRESHOLDS.proceed.minHighUrgency &&
|
|
141
|
-
betaTesters >= DECISION_THRESHOLDS.proceed.minBetaTesters) {
|
|
142
|
-
recommendation = 'proceed';
|
|
143
|
-
reasoning.push(`Strong signal: ${responses.length} responses, ${Math.round(highUrgencyPercent * 100)}% high urgency`);
|
|
144
|
-
reasoning.push(`${betaTesters} beta testers willing to help validate`);
|
|
145
|
-
reasoning.push(`Quality score: ${Math.round(qualityScore)}/100`);
|
|
146
|
-
} else if (responses.length >= DECISION_THRESHOLDS.phased.minResponses &&
|
|
147
|
-
betaTesters >= DECISION_THRESHOLDS.phased.minBetaTesters) {
|
|
148
|
-
recommendation = 'phased';
|
|
149
|
-
reasoning.push(`Moderate signal: ${responses.length} responses`);
|
|
150
|
-
reasoning.push('Consider phased approach starting with top-requested regulation');
|
|
151
|
-
|
|
152
|
-
const topReg = Object.entries(topRegulations).sort((a, b) => b[1] - a[1])[0];
|
|
153
|
-
if (topReg) {
|
|
154
|
-
reasoning.push(`Start with ${topReg[0]} (${topReg[1]} requests)`);
|
|
155
|
-
}
|
|
156
|
-
} else {
|
|
157
|
-
recommendation = 'defer';
|
|
158
|
-
reasoning.push(`Insufficient signal: ${responses.length} responses (need ${DECISION_THRESHOLDS.phased.minResponses}+)`);
|
|
159
|
-
reasoning.push(`High urgency: ${Math.round(highUrgencyPercent * 100)}%`);
|
|
160
|
-
reasoning.push(`Beta testers: ${betaTesters}`);
|
|
161
|
-
reasoning.push('Consider focusing on other features');
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return {
|
|
165
|
-
totalResponses: responses.length,
|
|
166
|
-
urgencyBreakdown,
|
|
167
|
-
topRegulations,
|
|
168
|
-
betaTesters,
|
|
169
|
-
potentialSponsors,
|
|
170
|
-
qualityScore,
|
|
171
|
-
recommendation,
|
|
172
|
-
reasoning,
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Format analysis results for display
|
|
178
|
-
*/
|
|
179
|
-
function formatResults(results: AnalysisResults): string {
|
|
180
|
-
const sections: string[] = [];
|
|
181
|
-
|
|
182
|
-
sections.push('# Survey Analysis Results\n');
|
|
183
|
-
sections.push(`**Total Responses:** ${results.totalResponses}`);
|
|
184
|
-
sections.push(`**Quality Score:** ${Math.round(results.qualityScore)}/100`);
|
|
185
|
-
sections.push(`**Recommendation:** ${results.recommendation.toUpperCase()}\n`);
|
|
186
|
-
|
|
187
|
-
sections.push('## Urgency Breakdown\n');
|
|
188
|
-
for (const [level, count] of Object.entries(results.urgencyBreakdown)) {
|
|
189
|
-
const percent = Math.round((count / results.totalResponses) * 100);
|
|
190
|
-
sections.push(`- ${level}: ${count} (${percent}%)`);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
sections.push('\n## Top Requested Regulations\n');
|
|
194
|
-
const sortedRegs = Object.entries(results.topRegulations)
|
|
195
|
-
.sort((a, b) => b[1] - a[1])
|
|
196
|
-
.slice(0, 5);
|
|
197
|
-
for (const [reg, count] of sortedRegs) {
|
|
198
|
-
sections.push(`- ${reg}: ${count} requests`);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
sections.push('\n## Engagement\n');
|
|
202
|
-
sections.push(`- Willing to beta test: ${results.betaTesters}`);
|
|
203
|
-
sections.push(`- Potential sponsors: ${results.potentialSponsors}`);
|
|
204
|
-
|
|
205
|
-
sections.push('\n## Recommendation Reasoning\n');
|
|
206
|
-
for (const reason of results.reasoning) {
|
|
207
|
-
sections.push(`- ${reason}`);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
return sections.join('\n');
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Fetch survey responses from GitHub Discussion (placeholder)
|
|
215
|
-
*/
|
|
216
|
-
async function fetchResponses(discussionNumber: number): Promise<SurveyResponse[]> {
|
|
217
|
-
// This is a placeholder - actual implementation would use GitHub API
|
|
218
|
-
console.log(`Fetching responses from discussion #${discussionNumber}...`);
|
|
219
|
-
|
|
220
|
-
// In production, this would use:
|
|
221
|
-
// - GitHub GraphQL API to fetch discussion comments
|
|
222
|
-
// - Parse structured form responses
|
|
223
|
-
// - Return array of SurveyResponse objects
|
|
224
|
-
|
|
225
|
-
return [];
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Main function
|
|
230
|
-
*/
|
|
231
|
-
async function main() {
|
|
232
|
-
const discussionNumber = parseInt(process.argv[2] || '0', 10);
|
|
233
|
-
|
|
234
|
-
if (discussionNumber === 0) {
|
|
235
|
-
console.log('Usage: npx tsx scripts/analyze-survey-responses.ts [discussion-number]');
|
|
236
|
-
console.log('\nExample: npx tsx scripts/analyze-survey-responses.ts 42');
|
|
237
|
-
console.log('\nYou can find the discussion number in the URL:');
|
|
238
|
-
console.log('https://github.com/owner/repo/discussions/42');
|
|
239
|
-
console.log(' ^^');
|
|
240
|
-
process.exit(1);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
console.log('EU Regulations MCP - Survey Response Analyzer');
|
|
244
|
-
console.log('='.repeat(50));
|
|
245
|
-
console.log();
|
|
246
|
-
|
|
247
|
-
// Fetch responses
|
|
248
|
-
const responses = await fetchResponses(discussionNumber);
|
|
249
|
-
|
|
250
|
-
if (responses.length === 0) {
|
|
251
|
-
console.log('⚠️ No responses found yet.');
|
|
252
|
-
console.log();
|
|
253
|
-
console.log('This could mean:');
|
|
254
|
-
console.log('- Survey just launched');
|
|
255
|
-
console.log('- Wrong discussion number');
|
|
256
|
-
console.log('- API token issues');
|
|
257
|
-
console.log();
|
|
258
|
-
console.log('Manual tracking in: docs/demand-validation-2026-q1.md');
|
|
259
|
-
process.exit(0);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Analyze responses
|
|
263
|
-
const results = analyzeResponses(responses);
|
|
264
|
-
|
|
265
|
-
// Display results
|
|
266
|
-
console.log(formatResults(results));
|
|
267
|
-
console.log();
|
|
268
|
-
console.log('='.repeat(50));
|
|
269
|
-
console.log(`Analysis complete. ${results.totalResponses} responses analyzed.`);
|
|
270
|
-
console.log();
|
|
271
|
-
console.log('Next steps:');
|
|
272
|
-
console.log('1. Update docs/demand-validation-2026-q1.md with these results');
|
|
273
|
-
console.log('2. Conduct user interviews with high-urgency respondents');
|
|
274
|
-
console.log('3. Review decision criteria after survey closes');
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Run if called directly
|
|
278
|
-
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
279
|
-
main().catch((error) => {
|
|
280
|
-
console.error('Error:', error);
|
|
281
|
-
process.exit(1);
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
export { analyzeResponses, parseResponse, type SurveyResponse, type AnalysisResults };
|
package/scripts/build-db.ts
DELETED
|
@@ -1,421 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env npx tsx
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Build the regulations.db SQLite database from seed JSON files.
|
|
5
|
-
* Run with: npm run build:db
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import Database from 'better-sqlite3';
|
|
9
|
-
import { readFileSync, existsSync, mkdirSync, unlinkSync, readdirSync } from 'fs';
|
|
10
|
-
import { join, dirname } from 'path';
|
|
11
|
-
import { fileURLToPath } from 'url';
|
|
12
|
-
|
|
13
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
-
const __dirname = dirname(__filename);
|
|
15
|
-
|
|
16
|
-
const DATA_DIR = join(__dirname, '..', 'data');
|
|
17
|
-
const SEED_DIR = join(DATA_DIR, 'seed');
|
|
18
|
-
const DB_PATH = join(DATA_DIR, 'regulations.db');
|
|
19
|
-
|
|
20
|
-
const SCHEMA = `
|
|
21
|
-
-- Core regulation metadata
|
|
22
|
-
CREATE TABLE IF NOT EXISTS regulations (
|
|
23
|
-
id TEXT PRIMARY KEY,
|
|
24
|
-
full_name TEXT NOT NULL,
|
|
25
|
-
celex_id TEXT NOT NULL,
|
|
26
|
-
effective_date TEXT,
|
|
27
|
-
last_amended TEXT,
|
|
28
|
-
eur_lex_url TEXT
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
-- Articles table
|
|
32
|
-
CREATE TABLE IF NOT EXISTS articles (
|
|
33
|
-
rowid INTEGER PRIMARY KEY,
|
|
34
|
-
regulation TEXT NOT NULL REFERENCES regulations(id),
|
|
35
|
-
article_number TEXT NOT NULL,
|
|
36
|
-
title TEXT,
|
|
37
|
-
text TEXT NOT NULL,
|
|
38
|
-
chapter TEXT,
|
|
39
|
-
recitals TEXT,
|
|
40
|
-
cross_references TEXT,
|
|
41
|
-
UNIQUE(regulation, article_number)
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
-- FTS5 virtual table for full-text search
|
|
45
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS articles_fts USING fts5(
|
|
46
|
-
regulation,
|
|
47
|
-
article_number,
|
|
48
|
-
title,
|
|
49
|
-
text,
|
|
50
|
-
content='articles',
|
|
51
|
-
content_rowid='rowid'
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
-- FTS5 triggers
|
|
55
|
-
CREATE TRIGGER IF NOT EXISTS articles_ai AFTER INSERT ON articles BEGIN
|
|
56
|
-
INSERT INTO articles_fts(rowid, regulation, article_number, title, text)
|
|
57
|
-
VALUES (new.rowid, new.regulation, new.article_number, new.title, new.text);
|
|
58
|
-
END;
|
|
59
|
-
|
|
60
|
-
CREATE TRIGGER IF NOT EXISTS articles_ad AFTER DELETE ON articles BEGIN
|
|
61
|
-
INSERT INTO articles_fts(articles_fts, rowid, regulation, article_number, title, text)
|
|
62
|
-
VALUES('delete', old.rowid, old.regulation, old.article_number, old.title, old.text);
|
|
63
|
-
END;
|
|
64
|
-
|
|
65
|
-
CREATE TRIGGER IF NOT EXISTS articles_au AFTER UPDATE ON articles BEGIN
|
|
66
|
-
INSERT INTO articles_fts(articles_fts, rowid, regulation, article_number, title, text)
|
|
67
|
-
VALUES('delete', old.rowid, old.regulation, old.article_number, old.title, old.text);
|
|
68
|
-
INSERT INTO articles_fts(rowid, regulation, article_number, title, text)
|
|
69
|
-
VALUES (new.rowid, new.regulation, new.article_number, new.title, new.text);
|
|
70
|
-
END;
|
|
71
|
-
|
|
72
|
-
-- Definitions
|
|
73
|
-
CREATE TABLE IF NOT EXISTS definitions (
|
|
74
|
-
id INTEGER PRIMARY KEY,
|
|
75
|
-
regulation TEXT NOT NULL REFERENCES regulations(id),
|
|
76
|
-
term TEXT NOT NULL,
|
|
77
|
-
definition TEXT NOT NULL,
|
|
78
|
-
article TEXT NOT NULL,
|
|
79
|
-
UNIQUE(regulation, term)
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
-- Control mappings
|
|
83
|
-
CREATE TABLE IF NOT EXISTS control_mappings (
|
|
84
|
-
id INTEGER PRIMARY KEY,
|
|
85
|
-
framework TEXT NOT NULL DEFAULT 'ISO27001',
|
|
86
|
-
control_id TEXT NOT NULL,
|
|
87
|
-
control_name TEXT NOT NULL,
|
|
88
|
-
regulation TEXT NOT NULL REFERENCES regulations(id),
|
|
89
|
-
articles TEXT NOT NULL,
|
|
90
|
-
coverage TEXT CHECK(coverage IN ('full', 'partial', 'related')),
|
|
91
|
-
notes TEXT
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
-- Applicability rules
|
|
95
|
-
CREATE TABLE IF NOT EXISTS applicability_rules (
|
|
96
|
-
id INTEGER PRIMARY KEY,
|
|
97
|
-
regulation TEXT NOT NULL REFERENCES regulations(id),
|
|
98
|
-
sector TEXT NOT NULL,
|
|
99
|
-
subsector TEXT,
|
|
100
|
-
applies INTEGER NOT NULL,
|
|
101
|
-
confidence TEXT CHECK(confidence IN ('definite', 'likely', 'possible')),
|
|
102
|
-
basis_article TEXT,
|
|
103
|
-
notes TEXT
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
-- Source registry for tracking data quality
|
|
107
|
-
CREATE TABLE IF NOT EXISTS source_registry (
|
|
108
|
-
regulation TEXT PRIMARY KEY REFERENCES regulations(id),
|
|
109
|
-
celex_id TEXT NOT NULL,
|
|
110
|
-
eur_lex_version TEXT,
|
|
111
|
-
last_fetched TEXT,
|
|
112
|
-
articles_expected INTEGER,
|
|
113
|
-
articles_parsed INTEGER,
|
|
114
|
-
quality_status TEXT CHECK(quality_status IN ('complete', 'review', 'incomplete')),
|
|
115
|
-
notes TEXT
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
-- Recitals table
|
|
119
|
-
CREATE TABLE IF NOT EXISTS recitals (
|
|
120
|
-
id INTEGER PRIMARY KEY,
|
|
121
|
-
regulation TEXT NOT NULL REFERENCES regulations(id),
|
|
122
|
-
recital_number INTEGER NOT NULL,
|
|
123
|
-
text TEXT NOT NULL,
|
|
124
|
-
related_articles TEXT,
|
|
125
|
-
UNIQUE(regulation, recital_number)
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
-- FTS5 virtual table for recital search
|
|
129
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS recitals_fts USING fts5(
|
|
130
|
-
regulation,
|
|
131
|
-
recital_number,
|
|
132
|
-
text,
|
|
133
|
-
content='recitals',
|
|
134
|
-
content_rowid='id'
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
-- FTS5 triggers for recitals
|
|
138
|
-
CREATE TRIGGER IF NOT EXISTS recitals_ai AFTER INSERT ON recitals BEGIN
|
|
139
|
-
INSERT INTO recitals_fts(rowid, regulation, recital_number, text)
|
|
140
|
-
VALUES (new.id, new.regulation, new.recital_number, new.text);
|
|
141
|
-
END;
|
|
142
|
-
|
|
143
|
-
CREATE TRIGGER IF NOT EXISTS recitals_ad AFTER DELETE ON recitals BEGIN
|
|
144
|
-
INSERT INTO recitals_fts(recitals_fts, rowid, regulation, recital_number, text)
|
|
145
|
-
VALUES('delete', old.id, old.regulation, old.recital_number, old.text);
|
|
146
|
-
END;
|
|
147
|
-
|
|
148
|
-
CREATE TRIGGER IF NOT EXISTS recitals_au AFTER UPDATE ON recitals BEGIN
|
|
149
|
-
INSERT INTO recitals_fts(recitals_fts, rowid, regulation, recital_number, text)
|
|
150
|
-
VALUES('delete', old.id, old.regulation, old.recital_number, old.text);
|
|
151
|
-
INSERT INTO recitals_fts(rowid, regulation, recital_number, text)
|
|
152
|
-
VALUES (new.id, new.regulation, new.recital_number, new.text);
|
|
153
|
-
END;
|
|
154
|
-
|
|
155
|
-
-- Evidence requirements table
|
|
156
|
-
CREATE TABLE IF NOT EXISTS evidence_requirements (
|
|
157
|
-
id INTEGER PRIMARY KEY,
|
|
158
|
-
regulation TEXT NOT NULL REFERENCES regulations(id),
|
|
159
|
-
article TEXT NOT NULL,
|
|
160
|
-
requirement_summary TEXT NOT NULL,
|
|
161
|
-
evidence_type TEXT NOT NULL CHECK(evidence_type IN ('document', 'log', 'test_result', 'certification', 'policy', 'procedure')),
|
|
162
|
-
artifact_name TEXT NOT NULL,
|
|
163
|
-
artifact_example TEXT,
|
|
164
|
-
description TEXT,
|
|
165
|
-
retention_period TEXT,
|
|
166
|
-
auditor_questions TEXT,
|
|
167
|
-
maturity_levels TEXT,
|
|
168
|
-
cross_references TEXT
|
|
169
|
-
);
|
|
170
|
-
`;
|
|
171
|
-
|
|
172
|
-
interface RegulationSeed {
|
|
173
|
-
id: string;
|
|
174
|
-
full_name: string;
|
|
175
|
-
celex_id: string;
|
|
176
|
-
effective_date?: string;
|
|
177
|
-
eur_lex_url?: string;
|
|
178
|
-
articles: Array<{
|
|
179
|
-
number: string;
|
|
180
|
-
title?: string;
|
|
181
|
-
text: string;
|
|
182
|
-
chapter?: string;
|
|
183
|
-
recitals?: string[];
|
|
184
|
-
cross_references?: string[];
|
|
185
|
-
}>;
|
|
186
|
-
definitions?: Array<{
|
|
187
|
-
term: string;
|
|
188
|
-
definition: string;
|
|
189
|
-
article: string;
|
|
190
|
-
}>;
|
|
191
|
-
recitals?: Array<{
|
|
192
|
-
recital_number: number;
|
|
193
|
-
text: string;
|
|
194
|
-
related_articles?: string;
|
|
195
|
-
}>;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function buildDatabase() {
|
|
199
|
-
console.log('Building regulations database...');
|
|
200
|
-
|
|
201
|
-
// Ensure data directory exists
|
|
202
|
-
if (!existsSync(DATA_DIR)) {
|
|
203
|
-
mkdirSync(DATA_DIR, { recursive: true });
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Delete existing database
|
|
207
|
-
if (existsSync(DB_PATH)) {
|
|
208
|
-
console.log('Removing existing database...');
|
|
209
|
-
unlinkSync(DB_PATH);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Create new database
|
|
213
|
-
const db = new Database(DB_PATH);
|
|
214
|
-
db.pragma('foreign_keys = ON');
|
|
215
|
-
|
|
216
|
-
// Create schema
|
|
217
|
-
console.log('Creating schema...');
|
|
218
|
-
db.exec(SCHEMA);
|
|
219
|
-
|
|
220
|
-
// Load and insert seed files
|
|
221
|
-
if (existsSync(SEED_DIR)) {
|
|
222
|
-
const seedFiles = readdirSync(SEED_DIR).filter((f: string) => f.endsWith('.json'));
|
|
223
|
-
|
|
224
|
-
for (const file of seedFiles) {
|
|
225
|
-
if (file.startsWith('mappings')) continue;
|
|
226
|
-
|
|
227
|
-
console.log(`Loading ${file}...`);
|
|
228
|
-
const content = readFileSync(join(SEED_DIR, file), 'utf-8');
|
|
229
|
-
const regulation: RegulationSeed = JSON.parse(content);
|
|
230
|
-
|
|
231
|
-
// Insert regulation
|
|
232
|
-
db.prepare(`
|
|
233
|
-
INSERT INTO regulations (id, full_name, celex_id, effective_date, eur_lex_url)
|
|
234
|
-
VALUES (?, ?, ?, ?, ?)
|
|
235
|
-
`).run(
|
|
236
|
-
regulation.id,
|
|
237
|
-
regulation.full_name,
|
|
238
|
-
regulation.celex_id,
|
|
239
|
-
regulation.effective_date || null,
|
|
240
|
-
regulation.eur_lex_url || null
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
// Insert articles
|
|
244
|
-
const insertArticle = db.prepare(`
|
|
245
|
-
INSERT INTO articles (regulation, article_number, title, text, chapter, recitals, cross_references)
|
|
246
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
247
|
-
`);
|
|
248
|
-
|
|
249
|
-
for (const article of regulation.articles) {
|
|
250
|
-
insertArticle.run(
|
|
251
|
-
regulation.id,
|
|
252
|
-
article.number,
|
|
253
|
-
article.title || null,
|
|
254
|
-
article.text,
|
|
255
|
-
article.chapter || null,
|
|
256
|
-
article.recitals ? JSON.stringify(article.recitals) : null,
|
|
257
|
-
article.cross_references ? JSON.stringify(article.cross_references) : null
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Insert definitions
|
|
262
|
-
if (regulation.definitions) {
|
|
263
|
-
const insertDefinition = db.prepare(`
|
|
264
|
-
INSERT OR IGNORE INTO definitions (regulation, term, definition, article)
|
|
265
|
-
VALUES (?, ?, ?, ?)
|
|
266
|
-
`);
|
|
267
|
-
|
|
268
|
-
for (const def of regulation.definitions) {
|
|
269
|
-
insertDefinition.run(regulation.id, def.term, def.definition, def.article);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Insert recitals
|
|
274
|
-
if (regulation.recitals) {
|
|
275
|
-
const insertRecital = db.prepare(`
|
|
276
|
-
INSERT OR IGNORE INTO recitals (regulation, recital_number, text, related_articles)
|
|
277
|
-
VALUES (?, ?, ?, ?)
|
|
278
|
-
`);
|
|
279
|
-
|
|
280
|
-
for (const recital of regulation.recitals) {
|
|
281
|
-
insertRecital.run(
|
|
282
|
-
regulation.id,
|
|
283
|
-
recital.recital_number,
|
|
284
|
-
recital.text,
|
|
285
|
-
recital.related_articles ? JSON.stringify(recital.related_articles) : null
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Update source registry with timestamps
|
|
291
|
-
const now = new Date().toISOString();
|
|
292
|
-
const eurLexVersion = regulation.effective_date || now.split('T')[0];
|
|
293
|
-
db.prepare(`
|
|
294
|
-
INSERT INTO source_registry (regulation, celex_id, eur_lex_version, last_fetched, articles_expected, articles_parsed, quality_status)
|
|
295
|
-
VALUES (?, ?, ?, ?, ?, ?, 'complete')
|
|
296
|
-
`).run(regulation.id, regulation.celex_id, eurLexVersion, now, regulation.articles.length, regulation.articles.length);
|
|
297
|
-
|
|
298
|
-
console.log(` Loaded ${regulation.articles.length} articles, ${regulation.definitions?.length || 0} definitions`);
|
|
299
|
-
if (regulation.recitals && regulation.recitals.length > 0) {
|
|
300
|
-
console.log(` Loaded ${regulation.recitals.length} recitals`);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Load mappings
|
|
305
|
-
const mappingsDir = join(SEED_DIR, 'mappings');
|
|
306
|
-
if (existsSync(mappingsDir)) {
|
|
307
|
-
const mappingFiles = readdirSync(mappingsDir).filter((f: string) => f.endsWith('.json'));
|
|
308
|
-
|
|
309
|
-
for (const file of mappingFiles) {
|
|
310
|
-
console.log(`Loading mappings from ${file}...`);
|
|
311
|
-
const content = readFileSync(join(mappingsDir, file), 'utf-8');
|
|
312
|
-
const mappings = JSON.parse(content);
|
|
313
|
-
|
|
314
|
-
// Detect framework from filename
|
|
315
|
-
let framework = 'ISO27001';
|
|
316
|
-
if (file.startsWith('nist-csf-')) {
|
|
317
|
-
framework = 'NIST_CSF';
|
|
318
|
-
} else if (file.startsWith('iso27001-')) {
|
|
319
|
-
framework = 'ISO27001';
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const insertMapping = db.prepare(`
|
|
323
|
-
INSERT INTO control_mappings (framework, control_id, control_name, regulation, articles, coverage, notes)
|
|
324
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
325
|
-
`);
|
|
326
|
-
|
|
327
|
-
for (const mapping of mappings) {
|
|
328
|
-
insertMapping.run(
|
|
329
|
-
framework,
|
|
330
|
-
mapping.control_id,
|
|
331
|
-
mapping.control_name,
|
|
332
|
-
mapping.regulation,
|
|
333
|
-
JSON.stringify(mapping.articles),
|
|
334
|
-
mapping.coverage,
|
|
335
|
-
mapping.notes || null
|
|
336
|
-
);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
console.log(` Loaded ${mappings.length} ${framework} control mappings`);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// Load applicability rules
|
|
344
|
-
const applicabilityDir = join(SEED_DIR, 'applicability');
|
|
345
|
-
if (existsSync(applicabilityDir)) {
|
|
346
|
-
const applicabilityFiles = readdirSync(applicabilityDir).filter((f: string) => f.endsWith('.json'));
|
|
347
|
-
|
|
348
|
-
const insertApplicability = db.prepare(`
|
|
349
|
-
INSERT INTO applicability_rules (regulation, sector, subsector, applies, confidence, basis_article, notes)
|
|
350
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
351
|
-
`);
|
|
352
|
-
|
|
353
|
-
for (const file of applicabilityFiles) {
|
|
354
|
-
console.log(`Loading applicability rules from ${file}...`);
|
|
355
|
-
const content = readFileSync(join(applicabilityDir, file), 'utf-8');
|
|
356
|
-
const rules = JSON.parse(content);
|
|
357
|
-
|
|
358
|
-
for (const rule of rules) {
|
|
359
|
-
insertApplicability.run(
|
|
360
|
-
rule.regulation,
|
|
361
|
-
rule.sector,
|
|
362
|
-
rule.subsector || null,
|
|
363
|
-
rule.applies ? 1 : 0,
|
|
364
|
-
rule.confidence,
|
|
365
|
-
rule.basis_article || null,
|
|
366
|
-
rule.notes || null
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
console.log(` Loaded ${rules.length} applicability rules`);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// Load evidence requirements
|
|
375
|
-
const evidenceDir = join(SEED_DIR, 'evidence');
|
|
376
|
-
if (existsSync(evidenceDir)) {
|
|
377
|
-
const evidenceFiles = readdirSync(evidenceDir).filter((f: string) => f.endsWith('.json'));
|
|
378
|
-
|
|
379
|
-
const insertEvidence = db.prepare(`
|
|
380
|
-
INSERT INTO evidence_requirements (
|
|
381
|
-
regulation, article, requirement_summary, evidence_type,
|
|
382
|
-
artifact_name, artifact_example, description, retention_period,
|
|
383
|
-
auditor_questions, maturity_levels, cross_references
|
|
384
|
-
)
|
|
385
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
386
|
-
`);
|
|
387
|
-
|
|
388
|
-
for (const file of evidenceFiles) {
|
|
389
|
-
console.log(`Loading evidence requirements from ${file}...`);
|
|
390
|
-
const content = readFileSync(join(evidenceDir, file), 'utf-8');
|
|
391
|
-
const requirements = JSON.parse(content);
|
|
392
|
-
|
|
393
|
-
for (const req of requirements) {
|
|
394
|
-
insertEvidence.run(
|
|
395
|
-
req.regulation,
|
|
396
|
-
req.article,
|
|
397
|
-
req.requirement_summary,
|
|
398
|
-
req.evidence_type,
|
|
399
|
-
req.artifact_name,
|
|
400
|
-
req.artifact_example || null,
|
|
401
|
-
req.description || null,
|
|
402
|
-
req.retention_period || null,
|
|
403
|
-
req.auditor_questions ? JSON.stringify(req.auditor_questions) : null,
|
|
404
|
-
req.maturity_levels ? JSON.stringify(req.maturity_levels) : null,
|
|
405
|
-
req.cross_references ? JSON.stringify(req.cross_references) : null
|
|
406
|
-
);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
console.log(` Loaded ${requirements.length} evidence requirements`);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
} else {
|
|
413
|
-
console.log('No seed directory found. Database created with empty tables.');
|
|
414
|
-
console.log(`Create seed files in: ${SEED_DIR}`);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
db.close();
|
|
418
|
-
console.log(`\nDatabase created at: ${DB_PATH}`);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
buildDatabase();
|