@ansvar/eu-regulations-mcp 0.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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +242 -0
  3. package/data/seed/ai-act.json +1026 -0
  4. package/data/seed/applicability/dora.json +92 -0
  5. package/data/seed/applicability/gdpr.json +74 -0
  6. package/data/seed/applicability/nis2.json +83 -0
  7. package/data/seed/cra.json +690 -0
  8. package/data/seed/cybersecurity-act.json +534 -0
  9. package/data/seed/dora.json +719 -0
  10. package/data/seed/gdpr.json +732 -0
  11. package/data/seed/mappings/iso27001-dora.json +106 -0
  12. package/data/seed/mappings/iso27001-gdpr.json +114 -0
  13. package/data/seed/mappings/iso27001-nis2.json +98 -0
  14. package/data/seed/nis2.json +492 -0
  15. package/dist/index.d.ts +3 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +271 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/tools/applicability.d.ts +20 -0
  20. package/dist/tools/applicability.d.ts.map +1 -0
  21. package/dist/tools/applicability.js +42 -0
  22. package/dist/tools/applicability.js.map +1 -0
  23. package/dist/tools/article.d.ts +17 -0
  24. package/dist/tools/article.d.ts.map +1 -0
  25. package/dist/tools/article.js +29 -0
  26. package/dist/tools/article.js.map +1 -0
  27. package/dist/tools/compare.d.ts +18 -0
  28. package/dist/tools/compare.d.ts.map +1 -0
  29. package/dist/tools/compare.js +60 -0
  30. package/dist/tools/compare.js.map +1 -0
  31. package/dist/tools/definitions.d.ts +14 -0
  32. package/dist/tools/definitions.d.ts.map +1 -0
  33. package/dist/tools/definitions.js +26 -0
  34. package/dist/tools/definitions.js.map +1 -0
  35. package/dist/tools/list.d.ts +22 -0
  36. package/dist/tools/list.d.ts.map +1 -0
  37. package/dist/tools/list.js +67 -0
  38. package/dist/tools/list.js.map +1 -0
  39. package/dist/tools/map.d.ts +19 -0
  40. package/dist/tools/map.d.ts.map +1 -0
  41. package/dist/tools/map.js +44 -0
  42. package/dist/tools/map.js.map +1 -0
  43. package/dist/tools/search.d.ts +15 -0
  44. package/dist/tools/search.d.ts.map +1 -0
  45. package/dist/tools/search.js +62 -0
  46. package/dist/tools/search.js.map +1 -0
  47. package/package.json +70 -0
  48. package/scripts/build-db.ts +292 -0
  49. package/scripts/check-updates.ts +192 -0
  50. package/scripts/ingest-eurlex.ts +219 -0
  51. package/src/index.ts +294 -0
  52. package/src/tools/applicability.ts +84 -0
  53. package/src/tools/article.ts +61 -0
  54. package/src/tools/compare.ts +94 -0
  55. package/src/tools/definitions.ts +54 -0
  56. package/src/tools/list.ts +116 -0
  57. package/src/tools/map.ts +84 -0
  58. package/src/tools/search.ts +95 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list.js","sourceRoot":"","sources":["../../src/tools/list.ts"],"names":[],"mappings":"AAyBA,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,EAAY,EACZ,KAAgB;IAEhB,MAAM,EAAE,UAAU,EAAE,GAAG,KAAK,CAAC;IAE7B,IAAI,UAAU,EAAE,CAAC;QACf,wCAAwC;QACxC,MAAM,MAAM,GAAG,EAAE,CAAC,OAAO,CAAC;;;;KAIzB,CAAC,CAAC,GAAG,CAAC,UAAU,CAKJ,CAAC;QAEd,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC;QAC7B,CAAC;QAED,kCAAkC;QAClC,MAAM,QAAQ,GAAG,EAAE,CAAC,OAAO,CAAC;;;;;KAK3B,CAAC,CAAC,GAAG,CAAC,UAAU,CAIf,CAAC;QAEH,mBAAmB;QACnB,MAAM,UAAU,GAAG,IAAI,GAAG,EAAmB,CAAC;QAC9C,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC/B,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,IAAI,SAAS,CAAC;YAChD,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;gBAChC,UAAU,CAAC,GAAG,CAAC,UAAU,EAAE;oBACzB,MAAM,EAAE,UAAU;oBAClB,KAAK,EAAE,WAAW,UAAU,EAAE;oBAC9B,QAAQ,EAAE,EAAE;iBACb,CAAC,CAAC;YACL,CAAC;YACD,UAAU,CAAC,GAAG,CAAC,UAAU,CAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;QACpE,CAAC;QAED,OAAO;YACL,WAAW,EAAE,CAAC;oBACZ,EAAE,EAAE,MAAM,CAAC,EAAE;oBACb,SAAS,EAAE,MAAM,CAAC,SAAS;oBAC3B,QAAQ,EAAE,MAAM,CAAC,QAAQ;oBACzB,cAAc,EAAE,MAAM,CAAC,cAAc;oBACrC,aAAa,EAAE,QAAQ,CAAC,MAAM;oBAC9B,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC;iBAC1C,CAAC;SACH,CAAC;IACJ,CAAC;IAED,2CAA2C;IAC3C,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC;;;;;;;;;;;GAWvB,CAAC,CAAC,GAAG,EAMJ,CAAC;IAEH,OAAO;QACL,WAAW,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAC5B,EAAE,EAAE,GAAG,CAAC,EAAE;YACV,SAAS,EAAE,GAAG,CAAC,SAAS;YACxB,QAAQ,EAAE,GAAG,CAAC,QAAQ;YACtB,cAAc,EAAE,GAAG,CAAC,cAAc;YAClC,aAAa,EAAE,GAAG,CAAC,aAAa;SACjC,CAAC,CAAC;KACJ,CAAC;AACJ,CAAC"}
@@ -0,0 +1,19 @@
1
+ import type { Database } from 'better-sqlite3';
2
+ export interface MapControlsInput {
3
+ framework: 'ISO27001';
4
+ control?: string;
5
+ regulation?: string;
6
+ }
7
+ export interface ControlMappingEntry {
8
+ regulation: string;
9
+ articles: string[];
10
+ coverage: 'full' | 'partial' | 'related';
11
+ notes: string | null;
12
+ }
13
+ export interface ControlMapping {
14
+ control_id: string;
15
+ control_name: string;
16
+ mappings: ControlMappingEntry[];
17
+ }
18
+ export declare function mapControls(db: Database, input: MapControlsInput): Promise<ControlMapping[]>;
19
+ //# sourceMappingURL=map.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"map.d.ts","sourceRoot":"","sources":["../../src/tools/map.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,UAAU,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAAC;IACzC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,mBAAmB,EAAE,CAAC;CACjC;AAED,wBAAsB,WAAW,CAC/B,EAAE,EAAE,QAAQ,EACZ,KAAK,EAAE,gBAAgB,GACtB,OAAO,CAAC,cAAc,EAAE,CAAC,CA2D3B"}
@@ -0,0 +1,44 @@
1
+ export async function mapControls(db, input) {
2
+ const { control, regulation } = input;
3
+ let sql = `
4
+ SELECT
5
+ control_id,
6
+ control_name,
7
+ regulation,
8
+ articles,
9
+ coverage,
10
+ notes
11
+ FROM control_mappings
12
+ WHERE 1=1
13
+ `;
14
+ const params = [];
15
+ if (control) {
16
+ sql += ` AND control_id = ?`;
17
+ params.push(control);
18
+ }
19
+ if (regulation) {
20
+ sql += ` AND regulation = ?`;
21
+ params.push(regulation);
22
+ }
23
+ sql += ` ORDER BY control_id, regulation`;
24
+ const rows = db.prepare(sql).all(...params);
25
+ // Group by control_id
26
+ const controlMap = new Map();
27
+ for (const row of rows) {
28
+ if (!controlMap.has(row.control_id)) {
29
+ controlMap.set(row.control_id, {
30
+ control_id: row.control_id,
31
+ control_name: row.control_name,
32
+ mappings: [],
33
+ });
34
+ }
35
+ controlMap.get(row.control_id).mappings.push({
36
+ regulation: row.regulation,
37
+ articles: JSON.parse(row.articles),
38
+ coverage: row.coverage,
39
+ notes: row.notes,
40
+ });
41
+ }
42
+ return Array.from(controlMap.values());
43
+ }
44
+ //# sourceMappingURL=map.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"map.js","sourceRoot":"","sources":["../../src/tools/map.ts"],"names":[],"mappings":"AAqBA,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,EAAY,EACZ,KAAuB;IAEvB,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,KAAK,CAAC;IAEtC,IAAI,GAAG,GAAG;;;;;;;;;;GAUT,CAAC;IAEF,MAAM,MAAM,GAAa,EAAE,CAAC;IAE5B,IAAI,OAAO,EAAE,CAAC;QACZ,GAAG,IAAI,qBAAqB,CAAC;QAC7B,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACvB,CAAC;IAED,IAAI,UAAU,EAAE,CAAC;QACf,GAAG,IAAI,qBAAqB,CAAC;QAC7B,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAC1B,CAAC;IAED,GAAG,IAAI,kCAAkC,CAAC;IAE1C,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,MAAM,CAOxC,CAAC;IAEH,sBAAsB;IACtB,MAAM,UAAU,GAAG,IAAI,GAAG,EAA0B,CAAC;IAErD,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;YACpC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,EAAE;gBAC7B,UAAU,EAAE,GAAG,CAAC,UAAU;gBAC1B,YAAY,EAAE,GAAG,CAAC,YAAY;gBAC9B,QAAQ,EAAE,EAAE;aACb,CAAC,CAAC;QACL,CAAC;QAED,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,CAAE,CAAC,QAAQ,CAAC,IAAI,CAAC;YAC5C,UAAU,EAAE,GAAG,CAAC,UAAU;YAC1B,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC;YAClC,QAAQ,EAAE,GAAG,CAAC,QAAQ;YACtB,KAAK,EAAE,GAAG,CAAC,KAAK;SACjB,CAAC,CAAC;IACL,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;AACzC,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { Database } from 'better-sqlite3';
2
+ export interface SearchInput {
3
+ query: string;
4
+ regulations?: string[];
5
+ limit?: number;
6
+ }
7
+ export interface SearchResult {
8
+ regulation: string;
9
+ article: string;
10
+ title: string;
11
+ snippet: string;
12
+ relevance: number;
13
+ }
14
+ export declare function searchRegulations(db: Database, input: SearchInput): Promise<SearchResult[]>;
15
+ //# sourceMappingURL=search.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/tools/search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAiBD,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,QAAQ,EACZ,KAAK,EAAE,WAAW,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC,CA4DzB"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Escape special FTS5 query characters to prevent syntax errors.
3
+ * FTS5 uses double quotes for phrase queries and has special operators.
4
+ */
5
+ function escapeFts5Query(query) {
6
+ // Remove characters that have special meaning in FTS5
7
+ // and wrap each word in double quotes for exact matching
8
+ return query
9
+ .replace(/['"]/g, '') // Remove quotes
10
+ .split(/\s+/)
11
+ .filter(word => word.length > 0)
12
+ .map(word => `"${word}"`)
13
+ .join(' ');
14
+ }
15
+ export async function searchRegulations(db, input) {
16
+ const { query, regulations, limit = 10 } = input;
17
+ if (!query || query.trim().length === 0) {
18
+ return [];
19
+ }
20
+ const escapedQuery = escapeFts5Query(query);
21
+ if (!escapedQuery) {
22
+ return [];
23
+ }
24
+ // Build the SQL query with optional regulation filter
25
+ let sql = `
26
+ SELECT
27
+ articles_fts.regulation,
28
+ articles_fts.article_number as article,
29
+ articles_fts.title,
30
+ snippet(articles_fts, 3, '>>>', '<<<', '...', 32) as snippet,
31
+ bm25(articles_fts) as relevance
32
+ FROM articles_fts
33
+ WHERE articles_fts MATCH ?
34
+ `;
35
+ const params = [escapedQuery];
36
+ if (regulations && regulations.length > 0) {
37
+ const placeholders = regulations.map(() => '?').join(', ');
38
+ sql += ` AND articles_fts.regulation IN (${placeholders})`;
39
+ params.push(...regulations);
40
+ }
41
+ // Order by relevance (bm25 returns negative scores, more negative = more relevant)
42
+ sql += ` ORDER BY bm25(articles_fts)`;
43
+ sql += ` LIMIT ?`;
44
+ params.push(limit);
45
+ try {
46
+ const stmt = db.prepare(sql);
47
+ const rows = stmt.all(...params);
48
+ // Convert bm25 scores to positive values (higher = more relevant)
49
+ return rows.map(row => ({
50
+ ...row,
51
+ relevance: Math.abs(row.relevance),
52
+ }));
53
+ }
54
+ catch (error) {
55
+ // If FTS5 query fails (e.g., syntax error), return empty results
56
+ if (error instanceof Error && error.message.includes('fts5')) {
57
+ return [];
58
+ }
59
+ throw error;
60
+ }
61
+ }
62
+ //# sourceMappingURL=search.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.js","sourceRoot":"","sources":["../../src/tools/search.ts"],"names":[],"mappings":"AAgBA;;;GAGG;AACH,SAAS,eAAe,CAAC,KAAa;IACpC,sDAAsD;IACtD,yDAAyD;IACzD,OAAO,KAAK;SACT,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,gBAAgB;SACrC,KAAK,CAAC,KAAK,CAAC;SACZ,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;SAC/B,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,IAAI,GAAG,CAAC;SACxB,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,EAAY,EACZ,KAAkB;IAElB,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,GAAG,EAAE,EAAE,GAAG,KAAK,CAAC;IAEjD,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxC,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,YAAY,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;IAE5C,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,sDAAsD;IACtD,IAAI,GAAG,GAAG;;;;;;;;;GAST,CAAC;IAEF,MAAM,MAAM,GAAwB,CAAC,YAAY,CAAC,CAAC;IAEnD,IAAI,WAAW,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1C,MAAM,YAAY,GAAG,WAAW,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3D,GAAG,IAAI,oCAAoC,YAAY,GAAG,CAAC;QAC3D,MAAM,CAAC,IAAI,CAAC,GAAG,WAAW,CAAC,CAAC;IAC9B,CAAC;IAED,mFAAmF;IACnF,GAAG,IAAI,8BAA8B,CAAC;IACtC,GAAG,IAAI,UAAU,CAAC;IAClB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAEnB,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC7B,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,MAAM,CAM7B,CAAC;QAEH,kEAAkE;QAClE,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACtB,GAAG,GAAG;YACN,SAAS,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC;SACnC,CAAC,CAAC,CAAC;IACN,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,iEAAiE;QACjE,IAAI,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YAC7D,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC"}
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@ansvar/eu-regulations-mcp",
3
+ "version": "0.1.0",
4
+ "description": "The first open-source MCP server for European cybersecurity regulations. Query DORA, NIS2, GDPR, EU AI Act, and more directly from Claude.",
5
+ "main": "dist/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "eu-regulations-mcp": "./dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "test": "vitest run",
13
+ "test:watch": "vitest",
14
+ "dev": "tsx src/index.ts",
15
+ "build:db": "tsx scripts/build-db.ts",
16
+ "ingest": "tsx scripts/ingest-eurlex.ts",
17
+ "check-updates": "tsx scripts/check-updates.ts",
18
+ "lint": "eslint src --ext .ts",
19
+ "postinstall": "test -f dist/index.js || npm run build"
20
+ },
21
+ "keywords": [
22
+ "mcp",
23
+ "model-context-protocol",
24
+ "compliance",
25
+ "gdpr",
26
+ "nis2",
27
+ "dora",
28
+ "ai-act",
29
+ "eu-regulations",
30
+ "cybersecurity",
31
+ "cyber-resilience-act",
32
+ "european-union",
33
+ "legal",
34
+ "regulatory",
35
+ "claude",
36
+ "llm"
37
+ ],
38
+ "author": "Ansvar Systems <hello@ansvar.ai> (https://ansvar.ai)",
39
+ "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/Ansvar-Systems/EU_compliance_MCP.git"
43
+ },
44
+ "bugs": {
45
+ "url": "https://github.com/Ansvar-Systems/EU_compliance_MCP/issues"
46
+ },
47
+ "homepage": "https://ansvar.ai",
48
+ "engines": {
49
+ "node": ">=18"
50
+ },
51
+ "files": [
52
+ "dist",
53
+ "data",
54
+ "scripts",
55
+ "src"
56
+ ],
57
+ "devDependencies": {
58
+ "@types/better-sqlite3": "^7.6.13",
59
+ "@types/jsdom": "^27.0.0",
60
+ "@types/node": "^25.0.10",
61
+ "tsx": "^4.21.0",
62
+ "typescript": "^5.9.3",
63
+ "vitest": "^4.0.18"
64
+ },
65
+ "dependencies": {
66
+ "@modelcontextprotocol/sdk": "^1.25.3",
67
+ "better-sqlite3": "^12.6.2",
68
+ "jsdom": "^27.4.0"
69
+ }
70
+ }
@@ -0,0 +1,292 @@
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
+ control_id TEXT NOT NULL,
86
+ control_name TEXT NOT NULL,
87
+ regulation TEXT NOT NULL REFERENCES regulations(id),
88
+ articles TEXT NOT NULL,
89
+ coverage TEXT CHECK(coverage IN ('full', 'partial', 'related')),
90
+ notes TEXT
91
+ );
92
+
93
+ -- Applicability rules
94
+ CREATE TABLE IF NOT EXISTS applicability_rules (
95
+ id INTEGER PRIMARY KEY,
96
+ regulation TEXT NOT NULL REFERENCES regulations(id),
97
+ sector TEXT NOT NULL,
98
+ subsector TEXT,
99
+ applies INTEGER NOT NULL,
100
+ confidence TEXT CHECK(confidence IN ('definite', 'likely', 'possible')),
101
+ basis_article TEXT,
102
+ notes TEXT
103
+ );
104
+
105
+ -- Source registry for tracking data quality
106
+ CREATE TABLE IF NOT EXISTS source_registry (
107
+ regulation TEXT PRIMARY KEY REFERENCES regulations(id),
108
+ celex_id TEXT NOT NULL,
109
+ eur_lex_version TEXT,
110
+ last_fetched TEXT,
111
+ articles_expected INTEGER,
112
+ articles_parsed INTEGER,
113
+ quality_status TEXT CHECK(quality_status IN ('complete', 'review', 'incomplete')),
114
+ notes TEXT
115
+ );
116
+ `;
117
+
118
+ interface RegulationSeed {
119
+ id: string;
120
+ full_name: string;
121
+ celex_id: string;
122
+ effective_date?: string;
123
+ eur_lex_url?: string;
124
+ articles: Array<{
125
+ number: string;
126
+ title?: string;
127
+ text: string;
128
+ chapter?: string;
129
+ recitals?: string[];
130
+ cross_references?: string[];
131
+ }>;
132
+ definitions?: Array<{
133
+ term: string;
134
+ definition: string;
135
+ article: string;
136
+ }>;
137
+ }
138
+
139
+ function buildDatabase() {
140
+ console.log('Building regulations database...');
141
+
142
+ // Ensure data directory exists
143
+ if (!existsSync(DATA_DIR)) {
144
+ mkdirSync(DATA_DIR, { recursive: true });
145
+ }
146
+
147
+ // Delete existing database
148
+ if (existsSync(DB_PATH)) {
149
+ console.log('Removing existing database...');
150
+ unlinkSync(DB_PATH);
151
+ }
152
+
153
+ // Create new database
154
+ const db = new Database(DB_PATH);
155
+ db.pragma('foreign_keys = ON');
156
+
157
+ // Create schema
158
+ console.log('Creating schema...');
159
+ db.exec(SCHEMA);
160
+
161
+ // Load and insert seed files
162
+ if (existsSync(SEED_DIR)) {
163
+ const seedFiles = readdirSync(SEED_DIR).filter((f: string) => f.endsWith('.json'));
164
+
165
+ for (const file of seedFiles) {
166
+ if (file.startsWith('mappings')) continue;
167
+
168
+ console.log(`Loading ${file}...`);
169
+ const content = readFileSync(join(SEED_DIR, file), 'utf-8');
170
+ const regulation: RegulationSeed = JSON.parse(content);
171
+
172
+ // Insert regulation
173
+ db.prepare(`
174
+ INSERT INTO regulations (id, full_name, celex_id, effective_date, eur_lex_url)
175
+ VALUES (?, ?, ?, ?, ?)
176
+ `).run(
177
+ regulation.id,
178
+ regulation.full_name,
179
+ regulation.celex_id,
180
+ regulation.effective_date || null,
181
+ regulation.eur_lex_url || null
182
+ );
183
+
184
+ // Insert articles
185
+ const insertArticle = db.prepare(`
186
+ INSERT INTO articles (regulation, article_number, title, text, chapter, recitals, cross_references)
187
+ VALUES (?, ?, ?, ?, ?, ?, ?)
188
+ `);
189
+
190
+ for (const article of regulation.articles) {
191
+ insertArticle.run(
192
+ regulation.id,
193
+ article.number,
194
+ article.title || null,
195
+ article.text,
196
+ article.chapter || null,
197
+ article.recitals ? JSON.stringify(article.recitals) : null,
198
+ article.cross_references ? JSON.stringify(article.cross_references) : null
199
+ );
200
+ }
201
+
202
+ // Insert definitions
203
+ if (regulation.definitions) {
204
+ const insertDefinition = db.prepare(`
205
+ INSERT INTO definitions (regulation, term, definition, article)
206
+ VALUES (?, ?, ?, ?)
207
+ `);
208
+
209
+ for (const def of regulation.definitions) {
210
+ insertDefinition.run(regulation.id, def.term, def.definition, def.article);
211
+ }
212
+ }
213
+
214
+ // Update source registry
215
+ db.prepare(`
216
+ INSERT INTO source_registry (regulation, celex_id, articles_expected, articles_parsed, quality_status)
217
+ VALUES (?, ?, ?, ?, 'complete')
218
+ `).run(regulation.id, regulation.celex_id, regulation.articles.length, regulation.articles.length);
219
+
220
+ console.log(` Loaded ${regulation.articles.length} articles, ${regulation.definitions?.length || 0} definitions`);
221
+ }
222
+
223
+ // Load mappings
224
+ const mappingsDir = join(SEED_DIR, 'mappings');
225
+ if (existsSync(mappingsDir)) {
226
+ const mappingFiles = readdirSync(mappingsDir).filter((f: string) => f.endsWith('.json'));
227
+
228
+ for (const file of mappingFiles) {
229
+ console.log(`Loading mappings from ${file}...`);
230
+ const content = readFileSync(join(mappingsDir, file), 'utf-8');
231
+ const mappings = JSON.parse(content);
232
+
233
+ const insertMapping = db.prepare(`
234
+ INSERT INTO control_mappings (control_id, control_name, regulation, articles, coverage, notes)
235
+ VALUES (?, ?, ?, ?, ?, ?)
236
+ `);
237
+
238
+ for (const mapping of mappings) {
239
+ insertMapping.run(
240
+ mapping.control_id,
241
+ mapping.control_name,
242
+ mapping.regulation,
243
+ JSON.stringify(mapping.articles),
244
+ mapping.coverage,
245
+ mapping.notes || null
246
+ );
247
+ }
248
+
249
+ console.log(` Loaded ${mappings.length} control mappings`);
250
+ }
251
+ }
252
+
253
+ // Load applicability rules
254
+ const applicabilityDir = join(SEED_DIR, 'applicability');
255
+ if (existsSync(applicabilityDir)) {
256
+ const applicabilityFiles = readdirSync(applicabilityDir).filter((f: string) => f.endsWith('.json'));
257
+
258
+ const insertApplicability = db.prepare(`
259
+ INSERT INTO applicability_rules (regulation, sector, subsector, applies, confidence, basis_article, notes)
260
+ VALUES (?, ?, ?, ?, ?, ?, ?)
261
+ `);
262
+
263
+ for (const file of applicabilityFiles) {
264
+ console.log(`Loading applicability rules from ${file}...`);
265
+ const content = readFileSync(join(applicabilityDir, file), 'utf-8');
266
+ const rules = JSON.parse(content);
267
+
268
+ for (const rule of rules) {
269
+ insertApplicability.run(
270
+ rule.regulation,
271
+ rule.sector,
272
+ rule.subsector || null,
273
+ rule.applies ? 1 : 0,
274
+ rule.confidence,
275
+ rule.basis_article || null,
276
+ rule.notes || null
277
+ );
278
+ }
279
+
280
+ console.log(` Loaded ${rules.length} applicability rules`);
281
+ }
282
+ }
283
+ } else {
284
+ console.log('No seed directory found. Database created with empty tables.');
285
+ console.log(`Create seed files in: ${SEED_DIR}`);
286
+ }
287
+
288
+ db.close();
289
+ console.log(`\nDatabase created at: ${DB_PATH}`);
290
+ }
291
+
292
+ buildDatabase();