@ansvar/us-regulations-mcp 1.0.0 → 1.2.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 +422 -79
- package/data/regulations.db +0 -0
- package/data/seed/colorado-cpa.json +97 -0
- package/data/seed/ffiec.json +103 -0
- package/data/seed/mappings/ccpa-nist-csf.json +11 -1
- package/data/seed/mappings/hipaa-nist-800-53.json +10 -1
- package/data/seed/nydfs.json +122 -0
- package/data/seed/sox.json +109 -0
- package/dist/index.js +1 -1
- package/dist/ingest/adapters/colorado-public.d.ts +25 -0
- package/dist/ingest/adapters/colorado-public.d.ts.map +1 -0
- package/dist/ingest/adapters/colorado-public.js +76 -0
- package/dist/ingest/adapters/colorado-public.js.map +1 -0
- package/dist/ingest/adapters/connecticut-cga.d.ts +22 -0
- package/dist/ingest/adapters/connecticut-cga.d.ts.map +1 -0
- package/dist/ingest/adapters/connecticut-cga.js +116 -0
- package/dist/ingest/adapters/connecticut-cga.js.map +1 -0
- package/dist/ingest/adapters/ecfr.d.ts +46 -4
- package/dist/ingest/adapters/ecfr.d.ts.map +1 -1
- package/dist/ingest/adapters/ecfr.js +131 -16
- package/dist/ingest/adapters/ecfr.js.map +1 -1
- package/dist/ingest/adapters/ffiec.d.ts +42 -0
- package/dist/ingest/adapters/ffiec.d.ts.map +1 -0
- package/dist/ingest/adapters/ffiec.js +68 -0
- package/dist/ingest/adapters/ffiec.js.map +1 -0
- package/dist/ingest/adapters/nydfs.d.ts +42 -0
- package/dist/ingest/adapters/nydfs.d.ts.map +1 -0
- package/dist/ingest/adapters/nydfs.js +68 -0
- package/dist/ingest/adapters/nydfs.js.map +1 -0
- package/dist/ingest/adapters/regulations-gov.d.ts +11 -12
- package/dist/ingest/adapters/regulations-gov.d.ts.map +1 -1
- package/dist/ingest/adapters/regulations-gov.js +46 -43
- package/dist/ingest/adapters/regulations-gov.js.map +1 -1
- package/dist/ingest/adapters/utah-xcode.d.ts +19 -0
- package/dist/ingest/adapters/utah-xcode.d.ts.map +1 -0
- package/dist/ingest/adapters/utah-xcode.js +112 -0
- package/dist/ingest/adapters/utah-xcode.js.map +1 -0
- package/dist/ingest/adapters/virginia-law.d.ts +21 -0
- package/dist/ingest/adapters/virginia-law.d.ts.map +1 -0
- package/dist/ingest/adapters/virginia-law.js +111 -0
- package/dist/ingest/adapters/virginia-law.js.map +1 -0
- package/package.json +26 -4
- package/scripts/build-db.ts +50 -32
- package/scripts/check-updates.ts +184 -0
- package/scripts/ingest.ts +72 -25
- package/src/index.ts +1 -1
- package/src/ingest/adapters/colorado-public.ts +96 -0
- package/src/ingest/adapters/connecticut-cga.ts +150 -0
- package/src/ingest/adapters/ecfr.ts +158 -17
- package/src/ingest/adapters/ffiec.ts +77 -0
- package/src/ingest/adapters/nydfs.ts +77 -0
- package/src/ingest/adapters/regulations-gov.ts +48 -47
- package/src/ingest/adapters/utah-xcode.ts +143 -0
- package/src/ingest/adapters/virginia-law.ts +140 -0
- package/scripts/quality-test.ts +0 -346
- package/scripts/test-mcp-tools.ts +0 -187
- package/scripts/test-remaining-tools.ts +0 -107
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utah XCode Adapter
|
|
3
|
+
*
|
|
4
|
+
* Fetches Utah UCPA from le.utah.gov with version resolution.
|
|
5
|
+
* Source: Utah Code Ann. § 13-61-101 to 13-61-404
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
SourceAdapter,
|
|
10
|
+
RegulationMetadata,
|
|
11
|
+
Section,
|
|
12
|
+
Definition,
|
|
13
|
+
UpdateStatus,
|
|
14
|
+
} from '../framework.js';
|
|
15
|
+
import * as cheerio from 'cheerio';
|
|
16
|
+
|
|
17
|
+
class ScrapingError extends Error {
|
|
18
|
+
constructor(message: string) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'ScrapingError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class UtahXcodeAdapter implements SourceAdapter {
|
|
25
|
+
private readonly regulationId = 'UTAH_UCPA';
|
|
26
|
+
private readonly sections = [
|
|
27
|
+
'101', '102', '103', '104', '201', '202', '203', '301', '302', '303', '304', '401', '402', '403', '404'
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
async fetchMetadata(): Promise<RegulationMetadata> {
|
|
31
|
+
return {
|
|
32
|
+
id: 'UTAH_UCPA',
|
|
33
|
+
full_name: 'Utah Consumer Privacy Act',
|
|
34
|
+
citation: 'Utah Code Ann. §13-61-101 to 13-61-404',
|
|
35
|
+
effective_date: '2023-12-31',
|
|
36
|
+
last_amended: '2024-05-01',
|
|
37
|
+
source_url: 'https://le.utah.gov/xcode/Title13/Chapter61/',
|
|
38
|
+
jurisdiction: 'utah',
|
|
39
|
+
regulation_type: 'statute',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async *fetchSections(): AsyncGenerator<Section[]> {
|
|
44
|
+
const sections: Section[] = [];
|
|
45
|
+
let totalCount = 0;
|
|
46
|
+
|
|
47
|
+
console.log(`Fetching Utah UCPA sections (two-step version resolution)...`);
|
|
48
|
+
|
|
49
|
+
for (const sectionNum of this.sections) {
|
|
50
|
+
try {
|
|
51
|
+
// Step 1: Get version reference
|
|
52
|
+
const indexUrl = `https://le.utah.gov/xcode/Title13/Chapter61/13-61-S${sectionNum}.html`;
|
|
53
|
+
|
|
54
|
+
await this.sleep(500);
|
|
55
|
+
const indexResponse = await fetch(indexUrl);
|
|
56
|
+
if (!indexResponse.ok) {
|
|
57
|
+
console.warn(` Skipping § 13-61-${sectionNum} (HTTP ${indexResponse.status})`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const indexHtml = await indexResponse.text();
|
|
62
|
+
|
|
63
|
+
// Extract version filename from JavaScript
|
|
64
|
+
const versionMatch = indexHtml.match(/versionArr\s*=\s*\[\s*\['([^']+)'/);
|
|
65
|
+
if (!versionMatch) {
|
|
66
|
+
console.warn(` Skipping § 13-61-${sectionNum} (no version found)`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const versionFile = versionMatch[1];
|
|
71
|
+
|
|
72
|
+
// Step 2: Fetch versioned HTML
|
|
73
|
+
const versionUrl = `https://le.utah.gov/xcode/Title13/Chapter61/${versionFile}`;
|
|
74
|
+
await this.sleep(500);
|
|
75
|
+
const versionResponse = await fetch(versionUrl);
|
|
76
|
+
|
|
77
|
+
if (!versionResponse.ok) {
|
|
78
|
+
console.warn(` Skipping § 13-61-${sectionNum} versioned file (HTTP ${versionResponse.status})`);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const versionHtml = await versionResponse.text();
|
|
83
|
+
const $ = cheerio.load(versionHtml);
|
|
84
|
+
|
|
85
|
+
const section = this.parseSection($, sectionNum);
|
|
86
|
+
if (section) {
|
|
87
|
+
sections.push(section);
|
|
88
|
+
totalCount++;
|
|
89
|
+
console.log(` Fetched § 13-61-${sectionNum}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (sections.length >= 10) {
|
|
93
|
+
yield sections.splice(0, 10);
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.warn(` Failed to fetch § 13-61-${sectionNum}, continuing...`, error);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (sections.length > 0) {
|
|
101
|
+
yield sections;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (totalCount < 10) {
|
|
105
|
+
throw new ScrapingError(`Expected at least 10 sections, got ${totalCount}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private parseSection($: cheerio.Root, sectionNum: string): Section | null {
|
|
110
|
+
// Utah uses a main content area
|
|
111
|
+
const content = $('body').text().trim();
|
|
112
|
+
|
|
113
|
+
if (!content || content.length < 200) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Extract title - format: "13-61-101 Definitions."
|
|
118
|
+
const titleMatch = content.match(/13-61-\d+\s+(.+?)\.(?:\n|$)/);
|
|
119
|
+
const title = titleMatch ? titleMatch[1].trim() : `Section 13-61-${sectionNum}`;
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
sectionNumber: `13-61-${sectionNum}`,
|
|
123
|
+
title: title,
|
|
124
|
+
text: content,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private async sleep(ms: number): Promise<void> {
|
|
129
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async extractDefinitions(): Promise<Definition[]> {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async checkForUpdates(lastFetched: Date): Promise<UpdateStatus> {
|
|
137
|
+
return { hasChanges: false };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function createUtahAdapter(): SourceAdapter {
|
|
142
|
+
return new UtahXcodeAdapter();
|
|
143
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Virginia Legislative Information System Adapter
|
|
3
|
+
*
|
|
4
|
+
* Fetches Virginia CDPA from Virginia LIS.
|
|
5
|
+
* Source: Va. Code Ann. § 59.1-575 to 59.1-585
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
SourceAdapter,
|
|
10
|
+
RegulationMetadata,
|
|
11
|
+
Section,
|
|
12
|
+
Definition,
|
|
13
|
+
UpdateStatus,
|
|
14
|
+
} from '../framework.js';
|
|
15
|
+
import * as cheerio from 'cheerio';
|
|
16
|
+
|
|
17
|
+
class ScrapingError extends Error {
|
|
18
|
+
constructor(message: string) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'ScrapingError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class VirginiaLawAdapter implements SourceAdapter {
|
|
25
|
+
private readonly regulationId = 'VIRGINIA_CDPA';
|
|
26
|
+
private readonly sectionStart = 575;
|
|
27
|
+
private readonly sectionEnd = 585;
|
|
28
|
+
|
|
29
|
+
async fetchMetadata(): Promise<RegulationMetadata> {
|
|
30
|
+
return {
|
|
31
|
+
id: 'VIRGINIA_CDPA',
|
|
32
|
+
full_name: 'Virginia Consumer Data Protection Act',
|
|
33
|
+
citation: 'Va. Code Ann. §59.1-575 to 59.1-585',
|
|
34
|
+
effective_date: '2023-01-01',
|
|
35
|
+
last_amended: '2026-01-01',
|
|
36
|
+
source_url: 'https://law.lis.virginia.gov/vacode/title59.1/chapter53/',
|
|
37
|
+
jurisdiction: 'virginia',
|
|
38
|
+
regulation_type: 'statute',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async *fetchSections(): AsyncGenerator<Section[]> {
|
|
43
|
+
const sections: Section[] = [];
|
|
44
|
+
let totalCount = 0;
|
|
45
|
+
|
|
46
|
+
console.log(`Fetching Virginia CDPA sections ${this.sectionStart}-${this.sectionEnd}...`);
|
|
47
|
+
|
|
48
|
+
for (let num = this.sectionStart; num <= this.sectionEnd; num++) {
|
|
49
|
+
try {
|
|
50
|
+
const url = `https://law.lis.virginia.gov/vacode/title59.1/chapter53/section59.1-${num}/`;
|
|
51
|
+
|
|
52
|
+
await this.sleep(500); // Polite delay
|
|
53
|
+
const response = await fetch(url);
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
console.warn(` Skipping § 59.1-${num} (HTTP ${response.status})`);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const html = await response.text();
|
|
60
|
+
const $ = cheerio.load(html);
|
|
61
|
+
|
|
62
|
+
this.validateDOM($);
|
|
63
|
+
|
|
64
|
+
const section = this.parseSection($, num);
|
|
65
|
+
if (section) {
|
|
66
|
+
sections.push(section);
|
|
67
|
+
totalCount++;
|
|
68
|
+
console.log(` Fetched § 59.1-${num}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (sections.length >= 10) {
|
|
72
|
+
yield sections.splice(0, 10);
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error instanceof ScrapingError) {
|
|
76
|
+
throw error; // Fail fast on DOM issues
|
|
77
|
+
}
|
|
78
|
+
console.warn(` Failed to fetch § 59.1-${num}, continuing...`, error);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (sections.length > 0) {
|
|
83
|
+
yield sections;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Validation
|
|
87
|
+
if (totalCount < 10) {
|
|
88
|
+
throw new ScrapingError(`Expected at least 10 sections, got ${totalCount}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private validateDOM($: cheerio.Root): void {
|
|
93
|
+
const codeContent = $('#va_code');
|
|
94
|
+
if (codeContent.length === 0) {
|
|
95
|
+
throw new ScrapingError('DOM structure changed: no #va_code element found');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private parseSection($: cheerio.Root, num: number): Section | null {
|
|
100
|
+
// Extract section text from #va_code
|
|
101
|
+
const textEl = $('#va_code');
|
|
102
|
+
const text = textEl.text().trim();
|
|
103
|
+
|
|
104
|
+
if (!text || text.length < 100) {
|
|
105
|
+
return null; // Skip sections with insufficient content
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Extract title from h2 within #va_code
|
|
109
|
+
const titleEl = textEl.find('h2').first();
|
|
110
|
+
const titleRaw = titleEl.text().trim();
|
|
111
|
+
|
|
112
|
+
// Parse title to extract just the description
|
|
113
|
+
// Format: "§ 59.1-575. (Effective January 1, 2026) Definitions."
|
|
114
|
+
const titleMatch = titleRaw.match(/§\s*59\.1-\d+\.?\s*(?:\([^)]+\))?\s*(.+?)\.?\s*$/);
|
|
115
|
+
const title = titleMatch ? titleMatch[1].trim() : titleRaw;
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
sectionNumber: `59.1-${num}`,
|
|
119
|
+
title: title,
|
|
120
|
+
text: text,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private async sleep(ms: number): Promise<void> {
|
|
125
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async extractDefinitions(): Promise<Definition[]> {
|
|
129
|
+
// No separate definitions for Virginia CDPA
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async checkForUpdates(lastFetched: Date): Promise<UpdateStatus> {
|
|
134
|
+
return { hasChanges: false };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function createVirginiaAdapter(): SourceAdapter {
|
|
139
|
+
return new VirginiaLawAdapter();
|
|
140
|
+
}
|
package/scripts/quality-test.ts
DELETED
|
@@ -1,346 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env npx tsx
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Comprehensive Quality Test for US Compliance MCP
|
|
5
|
-
* Tests various compliance queries to assess quality and coverage
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import Database from 'better-sqlite3';
|
|
9
|
-
import { join, dirname } from 'path';
|
|
10
|
-
import { fileURLToPath } from 'url';
|
|
11
|
-
import { searchRegulations } from '../src/tools/search.js';
|
|
12
|
-
import { getSection } from '../src/tools/section.js';
|
|
13
|
-
import { compareRequirements } from '../src/tools/compare.js';
|
|
14
|
-
import { mapControls } from '../src/tools/map.js';
|
|
15
|
-
import { checkApplicability } from '../src/tools/applicability.js';
|
|
16
|
-
|
|
17
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
18
|
-
const __dirname = dirname(__filename);
|
|
19
|
-
const DB_PATH = join(__dirname, '..', 'data', 'regulations.db');
|
|
20
|
-
|
|
21
|
-
const db = new Database(DB_PATH, { readonly: true });
|
|
22
|
-
|
|
23
|
-
interface TestCase {
|
|
24
|
-
name: string;
|
|
25
|
-
category: string;
|
|
26
|
-
test: () => Promise<any>;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const tests: TestCase[] = [];
|
|
30
|
-
let passed = 0;
|
|
31
|
-
let failed = 0;
|
|
32
|
-
|
|
33
|
-
function addTest(category: string, name: string, test: () => Promise<any>) {
|
|
34
|
-
tests.push({ category, name, test });
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// ===== HIPAA QUERIES =====
|
|
38
|
-
addTest('HIPAA', 'Security rule access controls', async () => {
|
|
39
|
-
const result = await searchRegulations(db, {
|
|
40
|
-
query: 'access control security',
|
|
41
|
-
regulation: 'HIPAA',
|
|
42
|
-
limit: 5
|
|
43
|
-
});
|
|
44
|
-
if (!result || result.length === 0) throw new Error('No results');
|
|
45
|
-
console.log(` 📋 Found ${result.length} sections`);
|
|
46
|
-
console.log(` 📝 Top result: ${result[0].section} - ${result[0].title}`);
|
|
47
|
-
return result;
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
addTest('HIPAA', 'Audit logs and ePHI access', async () => {
|
|
51
|
-
const result = await searchRegulations(db, {
|
|
52
|
-
query: 'audit log electronic protected health',
|
|
53
|
-
regulation: 'HIPAA',
|
|
54
|
-
limit: 5
|
|
55
|
-
});
|
|
56
|
-
if (!result || result.length === 0) throw new Error('No results');
|
|
57
|
-
console.log(` 📋 Found ${result.length} sections`);
|
|
58
|
-
console.log(` 📝 Top result: ${result[0].section}`);
|
|
59
|
-
return result;
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
addTest('HIPAA', 'Encryption requirements', async () => {
|
|
63
|
-
const result = await searchRegulations(db, {
|
|
64
|
-
query: 'encryption transmission storage',
|
|
65
|
-
regulation: 'HIPAA',
|
|
66
|
-
limit: 5
|
|
67
|
-
});
|
|
68
|
-
if (!result || result.length === 0) throw new Error('No results');
|
|
69
|
-
console.log(` 📋 Found ${result.length} sections`);
|
|
70
|
-
console.log(` 📝 Top result: ${result[0].section}`);
|
|
71
|
-
return result;
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
addTest('HIPAA', 'Breach notification requirements', async () => {
|
|
75
|
-
const result = await searchRegulations(db, {
|
|
76
|
-
query: 'breach notification timeline',
|
|
77
|
-
regulation: 'HIPAA',
|
|
78
|
-
limit: 5
|
|
79
|
-
});
|
|
80
|
-
if (!result || result.length === 0) throw new Error('No results');
|
|
81
|
-
console.log(` 📋 Found ${result.length} sections`);
|
|
82
|
-
console.log(` 📝 Top result: ${result[0].section}`);
|
|
83
|
-
return result;
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
addTest('HIPAA', 'Risk assessment requirements', async () => {
|
|
87
|
-
const result = await searchRegulations(db, {
|
|
88
|
-
query: 'risk assessment analysis',
|
|
89
|
-
regulation: 'HIPAA',
|
|
90
|
-
limit: 5
|
|
91
|
-
});
|
|
92
|
-
if (!result || result.length === 0) throw new Error('No results');
|
|
93
|
-
console.log(` 📋 Found ${result.length} sections`);
|
|
94
|
-
console.log(` 📝 Top result: ${result[0].section}`);
|
|
95
|
-
return result;
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// ===== CCPA QUERIES =====
|
|
99
|
-
addTest('CCPA', 'Consumer rights to data deletion', async () => {
|
|
100
|
-
const result = await searchRegulations(db, {
|
|
101
|
-
query: 'consumer right delete personal information',
|
|
102
|
-
regulation: 'CCPA',
|
|
103
|
-
limit: 5
|
|
104
|
-
});
|
|
105
|
-
if (!result || result.length === 0) throw new Error('No results');
|
|
106
|
-
console.log(` 📋 Found ${result.length} sections`);
|
|
107
|
-
console.log(` 📝 Top result: ${result[0].section}`);
|
|
108
|
-
return result;
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
addTest('CCPA', 'Data disclosure requirements', async () => {
|
|
112
|
-
const result = await searchRegulations(db, {
|
|
113
|
-
query: 'disclosure categories personal information',
|
|
114
|
-
regulation: 'CCPA',
|
|
115
|
-
limit: 5
|
|
116
|
-
});
|
|
117
|
-
if (!result || result.length === 0) throw new Error('No results');
|
|
118
|
-
console.log(` 📋 Found ${result.length} sections`);
|
|
119
|
-
console.log(` 📝 Top result: ${result[0].section}`);
|
|
120
|
-
return result;
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
addTest('CCPA', 'Do Not Sell opt-out', async () => {
|
|
124
|
-
const result = await searchRegulations(db, {
|
|
125
|
-
query: 'do not sell opt-out',
|
|
126
|
-
regulation: 'CCPA',
|
|
127
|
-
limit: 5
|
|
128
|
-
});
|
|
129
|
-
if (!result || result.length === 0) throw new Error('No results');
|
|
130
|
-
console.log(` 📋 Found ${result.length} sections`);
|
|
131
|
-
console.log(` 📝 Top result: ${result[0].section}`);
|
|
132
|
-
return result;
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// ===== SOX QUERIES =====
|
|
136
|
-
addTest('SOX', 'Section 404 IT controls', async () => {
|
|
137
|
-
const result = await searchRegulations(db, {
|
|
138
|
-
query: 'internal control information technology',
|
|
139
|
-
regulation: 'SOX',
|
|
140
|
-
limit: 5
|
|
141
|
-
});
|
|
142
|
-
if (!result || result.length === 0) throw new Error('No results');
|
|
143
|
-
console.log(` 📋 Found ${result.length} sections`);
|
|
144
|
-
console.log(` 📝 Top result: ${result[0].section}`);
|
|
145
|
-
return result;
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
addTest('SOX', 'Financial records retention', async () => {
|
|
149
|
-
const result = await searchRegulations(db, {
|
|
150
|
-
query: 'retention records audit',
|
|
151
|
-
regulation: 'SOX',
|
|
152
|
-
limit: 5
|
|
153
|
-
});
|
|
154
|
-
if (!result || result.length === 0) throw new Error('No results');
|
|
155
|
-
console.log(` 📋 Found ${result.length} sections`);
|
|
156
|
-
console.log(` 📝 Top result: ${result[0].section}`);
|
|
157
|
-
return result;
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
// ===== CROSS-REGULATION QUERIES =====
|
|
161
|
-
addTest('Cross-Regulation', 'Compare breach notification timelines', async () => {
|
|
162
|
-
const result = await compareRequirements(db, {
|
|
163
|
-
topic: 'breach notification timeline',
|
|
164
|
-
regulations: ['HIPAA', 'CCPA']
|
|
165
|
-
});
|
|
166
|
-
if (!result || !result.comparisons) throw new Error('No comparison results');
|
|
167
|
-
console.log(` 📊 Compared ${result.comparisons.length} regulations`);
|
|
168
|
-
for (const comp of result.comparisons) {
|
|
169
|
-
console.log(` - ${comp.regulation}: ${comp.matches?.length || 0} matches`);
|
|
170
|
-
}
|
|
171
|
-
return result;
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
addTest('Cross-Regulation', 'Compare incident response requirements', async () => {
|
|
175
|
-
const result = await compareRequirements(db, {
|
|
176
|
-
topic: 'incident response',
|
|
177
|
-
regulations: ['HIPAA', 'CCPA', 'SOX']
|
|
178
|
-
});
|
|
179
|
-
if (!result || !result.comparisons) throw new Error('No comparison results');
|
|
180
|
-
console.log(` 📊 Compared ${result.comparisons.length} regulations`);
|
|
181
|
-
for (const comp of result.comparisons) {
|
|
182
|
-
console.log(` - ${comp.regulation}: ${comp.matches?.length || 0} matches`);
|
|
183
|
-
}
|
|
184
|
-
return result;
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
addTest('Cross-Regulation', 'Data protection across all regulations', async () => {
|
|
188
|
-
const result = await compareRequirements(db, {
|
|
189
|
-
topic: 'data protection security',
|
|
190
|
-
regulations: ['HIPAA', 'CCPA', 'SOX']
|
|
191
|
-
});
|
|
192
|
-
if (!result || !result.comparisons) throw new Error('No comparison results');
|
|
193
|
-
console.log(` 📊 Compared ${result.comparisons.length} regulations`);
|
|
194
|
-
for (const comp of result.comparisons) {
|
|
195
|
-
console.log(` - ${comp.regulation}: ${comp.matches?.length || 0} matches`);
|
|
196
|
-
}
|
|
197
|
-
return result;
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
// ===== CONTROL MAPPINGS =====
|
|
201
|
-
addTest('Control Mappings', 'NIST 800-53 to HIPAA', async () => {
|
|
202
|
-
const result = await mapControls(db, {
|
|
203
|
-
framework: 'NIST_800_53_R5',
|
|
204
|
-
regulation: 'HIPAA'
|
|
205
|
-
});
|
|
206
|
-
if (!result.mappings || result.mappings.length === 0) throw new Error('No mappings');
|
|
207
|
-
console.log(` 🔗 Found ${result.mappings.length} mappings`);
|
|
208
|
-
console.log(` 📝 Sample: ${result.mappings[0].control_id} → ${result.mappings[0].regulation_sections?.slice(0, 3).join(', ')}`);
|
|
209
|
-
return result;
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
addTest('Control Mappings', 'NIST CSF to CCPA', async () => {
|
|
213
|
-
const result = await mapControls(db, {
|
|
214
|
-
framework: 'NIST_CSF_2_0',
|
|
215
|
-
regulation: 'CCPA'
|
|
216
|
-
});
|
|
217
|
-
if (!result.mappings || result.mappings.length === 0) throw new Error('No mappings');
|
|
218
|
-
console.log(` 🔗 Found ${result.mappings.length} mappings`);
|
|
219
|
-
console.log(` 📝 Sample: ${result.mappings[0].control_id}`);
|
|
220
|
-
return result;
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
// ===== APPLICABILITY CHECKS =====
|
|
224
|
-
addTest('Applicability', 'Healthcare sector', async () => {
|
|
225
|
-
const result = await checkApplicability(db, { sector: 'healthcare' });
|
|
226
|
-
if (!result.applicable_regulations) throw new Error('No results');
|
|
227
|
-
console.log(` ✅ ${result.applicable_regulations.length} applicable regulations`);
|
|
228
|
-
for (const reg of result.applicable_regulations) {
|
|
229
|
-
console.log(` - ${reg.regulation}: ${reg.confidence}`);
|
|
230
|
-
}
|
|
231
|
-
return result;
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
addTest('Applicability', 'Financial services', async () => {
|
|
235
|
-
const result = await checkApplicability(db, { sector: 'financial' });
|
|
236
|
-
if (!result.applicable_regulations) throw new Error('No results');
|
|
237
|
-
console.log(` ✅ ${result.applicable_regulations.length} applicable regulations`);
|
|
238
|
-
for (const reg of result.applicable_regulations) {
|
|
239
|
-
console.log(` - ${reg.regulation}: ${reg.confidence}`);
|
|
240
|
-
}
|
|
241
|
-
return result;
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
addTest('Applicability', 'E-commerce (California)', async () => {
|
|
245
|
-
const result = await checkApplicability(db, {
|
|
246
|
-
sector: 'technology',
|
|
247
|
-
details: 'e-commerce selling to California consumers'
|
|
248
|
-
});
|
|
249
|
-
if (!result.applicable_regulations) throw new Error('No results');
|
|
250
|
-
console.log(` ✅ ${result.applicable_regulations.length} applicable regulations`);
|
|
251
|
-
for (const reg of result.applicable_regulations) {
|
|
252
|
-
console.log(` - ${reg.regulation}: ${reg.confidence}`);
|
|
253
|
-
}
|
|
254
|
-
return result;
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
// ===== SECTION RETRIEVAL =====
|
|
258
|
-
addTest('Section Retrieval', 'HIPAA Security Rule 164.312', async () => {
|
|
259
|
-
const result = await getSection(db, { regulation: 'HIPAA', section: '164.312' });
|
|
260
|
-
if (!result || !result.text) throw new Error('Section not found');
|
|
261
|
-
console.log(` 📄 ${result.section_number}: ${result.title}`);
|
|
262
|
-
console.log(` 📊 Text length: ${result.text.length} chars`);
|
|
263
|
-
return result;
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
addTest('Section Retrieval', 'CCPA 1798.100', async () => {
|
|
267
|
-
const result = await getSection(db, { regulation: 'CCPA', section: '1798.100' });
|
|
268
|
-
if (!result || !result.text) throw new Error('Section not found');
|
|
269
|
-
console.log(` 📄 ${result.section_number}: ${result.title}`);
|
|
270
|
-
console.log(` 📊 Text length: ${result.text.length} chars`);
|
|
271
|
-
return result;
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
// ===== EDGE CASES =====
|
|
275
|
-
addTest('Edge Cases', 'Empty query handling', async () => {
|
|
276
|
-
try {
|
|
277
|
-
await searchRegulations(db, { query: '', limit: 5 });
|
|
278
|
-
throw new Error('Should have rejected empty query');
|
|
279
|
-
} catch (error: any) {
|
|
280
|
-
if (error.message.includes('Query cannot be empty')) {
|
|
281
|
-
console.log(' ✅ Correctly rejected empty query');
|
|
282
|
-
return { success: true };
|
|
283
|
-
}
|
|
284
|
-
throw error;
|
|
285
|
-
}
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
addTest('Edge Cases', 'Invalid regulation ID', async () => {
|
|
289
|
-
const result = await searchRegulations(db, {
|
|
290
|
-
query: 'test',
|
|
291
|
-
regulation: 'INVALID',
|
|
292
|
-
limit: 5
|
|
293
|
-
});
|
|
294
|
-
// Should return empty or handle gracefully
|
|
295
|
-
console.log(` ✅ Handled invalid regulation: ${result.length} results`);
|
|
296
|
-
return result;
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
// ===== RUN TESTS =====
|
|
300
|
-
async function runAllTests() {
|
|
301
|
-
console.log('🔍 US Compliance MCP - Comprehensive Quality Assessment\n');
|
|
302
|
-
console.log('=' .repeat(70));
|
|
303
|
-
|
|
304
|
-
let currentCategory = '';
|
|
305
|
-
|
|
306
|
-
for (const testCase of tests) {
|
|
307
|
-
if (testCase.category !== currentCategory) {
|
|
308
|
-
currentCategory = testCase.category;
|
|
309
|
-
console.log(`\n📂 ${currentCategory.toUpperCase()}`);
|
|
310
|
-
console.log('-'.repeat(70));
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
try {
|
|
314
|
-
console.log(`\n✨ ${testCase.name}`);
|
|
315
|
-
await testCase.test();
|
|
316
|
-
console.log(' ✅ PASSED');
|
|
317
|
-
passed++;
|
|
318
|
-
} catch (error: any) {
|
|
319
|
-
console.log(` ❌ FAILED: ${error.message}`);
|
|
320
|
-
failed++;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Final Summary
|
|
325
|
-
console.log('\n' + '='.repeat(70));
|
|
326
|
-
console.log('📊 FINAL RESULTS');
|
|
327
|
-
console.log('='.repeat(70));
|
|
328
|
-
console.log(`Total Tests: ${tests.length}`);
|
|
329
|
-
console.log(`✅ Passed: ${passed} (${Math.round(passed/tests.length * 100)}%)`);
|
|
330
|
-
console.log(`❌ Failed: ${failed} (${Math.round(failed/tests.length * 100)}%)`);
|
|
331
|
-
|
|
332
|
-
if (failed === 0) {
|
|
333
|
-
console.log('\n🎉 All quality tests passed! MCP is production-ready.\n');
|
|
334
|
-
} else {
|
|
335
|
-
console.log(`\n⚠️ ${failed} test(s) failed. Review errors above.\n`);
|
|
336
|
-
process.exit(1);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
db.close();
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
runAllTests().catch(error => {
|
|
343
|
-
console.error('Fatal error:', error);
|
|
344
|
-
db.close();
|
|
345
|
-
process.exit(1);
|
|
346
|
-
});
|