@ansvar/us-regulations-mcp 1.0.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 (89) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +275 -0
  3. package/data/.gitkeep +0 -0
  4. package/data/regulations.db +0 -0
  5. package/data/seed/applicability/rules.json +74 -0
  6. package/data/seed/mappings/ccpa-nist-csf.json +144 -0
  7. package/data/seed/mappings/hipaa-nist-800-53.json +377 -0
  8. package/dist/index.d.ts +3 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +41 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/ingest/adapters/california-leginfo.d.ts +72 -0
  13. package/dist/ingest/adapters/california-leginfo.d.ts.map +1 -0
  14. package/dist/ingest/adapters/california-leginfo.js +270 -0
  15. package/dist/ingest/adapters/california-leginfo.js.map +1 -0
  16. package/dist/ingest/adapters/ecfr.d.ts +76 -0
  17. package/dist/ingest/adapters/ecfr.d.ts.map +1 -0
  18. package/dist/ingest/adapters/ecfr.js +355 -0
  19. package/dist/ingest/adapters/ecfr.js.map +1 -0
  20. package/dist/ingest/adapters/regulations-gov.d.ts +47 -0
  21. package/dist/ingest/adapters/regulations-gov.d.ts.map +1 -0
  22. package/dist/ingest/adapters/regulations-gov.js +91 -0
  23. package/dist/ingest/adapters/regulations-gov.js.map +1 -0
  24. package/dist/ingest/framework.d.ts +84 -0
  25. package/dist/ingest/framework.d.ts.map +1 -0
  26. package/dist/ingest/framework.js +8 -0
  27. package/dist/ingest/framework.js.map +1 -0
  28. package/dist/tools/action-items.d.ts +23 -0
  29. package/dist/tools/action-items.d.ts.map +1 -0
  30. package/dist/tools/action-items.js +118 -0
  31. package/dist/tools/action-items.js.map +1 -0
  32. package/dist/tools/applicability.d.ts +26 -0
  33. package/dist/tools/applicability.d.ts.map +1 -0
  34. package/dist/tools/applicability.js +49 -0
  35. package/dist/tools/applicability.js.map +1 -0
  36. package/dist/tools/compare.d.ts +20 -0
  37. package/dist/tools/compare.d.ts.map +1 -0
  38. package/dist/tools/compare.js +35 -0
  39. package/dist/tools/compare.js.map +1 -0
  40. package/dist/tools/definitions.d.ts +22 -0
  41. package/dist/tools/definitions.d.ts.map +1 -0
  42. package/dist/tools/definitions.js +43 -0
  43. package/dist/tools/definitions.js.map +1 -0
  44. package/dist/tools/evidence.d.ts +23 -0
  45. package/dist/tools/evidence.d.ts.map +1 -0
  46. package/dist/tools/evidence.js +27 -0
  47. package/dist/tools/evidence.js.map +1 -0
  48. package/dist/tools/list.d.ts +25 -0
  49. package/dist/tools/list.d.ts.map +1 -0
  50. package/dist/tools/list.js +66 -0
  51. package/dist/tools/list.js.map +1 -0
  52. package/dist/tools/map.d.ts +26 -0
  53. package/dist/tools/map.d.ts.map +1 -0
  54. package/dist/tools/map.js +58 -0
  55. package/dist/tools/map.js.map +1 -0
  56. package/dist/tools/registry.d.ts +19 -0
  57. package/dist/tools/registry.d.ts.map +1 -0
  58. package/dist/tools/registry.js +260 -0
  59. package/dist/tools/registry.js.map +1 -0
  60. package/dist/tools/search.d.ts +15 -0
  61. package/dist/tools/search.d.ts.map +1 -0
  62. package/dist/tools/search.js +94 -0
  63. package/dist/tools/search.js.map +1 -0
  64. package/dist/tools/section.d.ts +19 -0
  65. package/dist/tools/section.d.ts.map +1 -0
  66. package/dist/tools/section.js +50 -0
  67. package/dist/tools/section.js.map +1 -0
  68. package/package.json +76 -0
  69. package/scripts/build-db.ts +268 -0
  70. package/scripts/ingest.ts +214 -0
  71. package/scripts/load-seed-data.ts +133 -0
  72. package/scripts/quality-test.ts +346 -0
  73. package/scripts/test-mcp-tools.ts +187 -0
  74. package/scripts/test-remaining-tools.ts +107 -0
  75. package/src/index.ts +55 -0
  76. package/src/ingest/adapters/california-leginfo.ts +322 -0
  77. package/src/ingest/adapters/ecfr.ts +403 -0
  78. package/src/ingest/adapters/regulations-gov.ts +112 -0
  79. package/src/ingest/framework.ts +92 -0
  80. package/src/tools/action-items.ts +164 -0
  81. package/src/tools/applicability.ts +91 -0
  82. package/src/tools/compare.ts +61 -0
  83. package/src/tools/definitions.ts +79 -0
  84. package/src/tools/evidence.ts +53 -0
  85. package/src/tools/list.ts +120 -0
  86. package/src/tools/map.ts +100 -0
  87. package/src/tools/registry.ts +275 -0
  88. package/src/tools/search.ts +132 -0
  89. package/src/tools/section.ts +85 -0
@@ -0,0 +1,164 @@
1
+ import type { Database } from 'better-sqlite3';
2
+ import { getSection, SectionData } from './section.js';
3
+
4
+ export interface ActionItemsInput {
5
+ regulation: string;
6
+ sections: string[];
7
+ }
8
+
9
+ export interface ActionItem {
10
+ section: string;
11
+ title: string;
12
+ required_state: string;
13
+ priority: 'high' | 'medium' | 'low';
14
+ evidence_needed: string[];
15
+ }
16
+
17
+ export interface ActionItemsResult {
18
+ regulation: string;
19
+ action_items: ActionItem[];
20
+ total_items: number;
21
+ }
22
+
23
+ /**
24
+ * Extract action items priority from section text.
25
+ * - High: Contains "shall", "must", "required"
26
+ * - Medium: Contains "should", "recommended"
27
+ * - Low: Everything else
28
+ */
29
+ function extractPriority(text: string): 'high' | 'medium' | 'low' {
30
+ const lowerText = text.toLowerCase();
31
+
32
+ if (
33
+ lowerText.includes('shall') ||
34
+ lowerText.includes('must') ||
35
+ lowerText.includes('required')
36
+ ) {
37
+ return 'high';
38
+ }
39
+
40
+ if (lowerText.includes('should') || lowerText.includes('recommended')) {
41
+ return 'medium';
42
+ }
43
+
44
+ return 'low';
45
+ }
46
+
47
+ /**
48
+ * Extract evidence keywords from section text.
49
+ * Looks for common audit artifacts and documentation types.
50
+ */
51
+ function extractEvidenceNeeded(text: string): string[] {
52
+ const lowerText = text.toLowerCase();
53
+ const evidence: Set<string> = new Set();
54
+
55
+ // Common evidence types
56
+ const evidenceKeywords: Record<string, string> = {
57
+ 'risk assessment': 'risk assessment report',
58
+ 'risk analysis': 'risk assessment report',
59
+ 'audit log': 'audit logs',
60
+ 'access log': 'access logs',
61
+ 'security incident': 'incident response documentation',
62
+ 'breach': 'breach notification records',
63
+ 'policy': 'written policies',
64
+ 'procedure': 'written procedures',
65
+ 'training': 'training records',
66
+ 'documentation': 'supporting documentation',
67
+ 'encryption': 'encryption documentation',
68
+ 'access control': 'access control matrix',
69
+ 'authentication': 'authentication logs',
70
+ 'authorization': 'authorization records',
71
+ 'backup': 'backup records',
72
+ 'disaster recovery': 'disaster recovery plan',
73
+ 'business continuity': 'business continuity plan',
74
+ };
75
+
76
+ for (const [keyword, evidenceType] of Object.entries(evidenceKeywords)) {
77
+ if (lowerText.includes(keyword)) {
78
+ evidence.add(evidenceType);
79
+ }
80
+ }
81
+
82
+ return Array.from(evidence);
83
+ }
84
+
85
+ /**
86
+ * Generate compliance action items from regulation sections.
87
+ * Extracts priority and evidence requirements from section text.
88
+ */
89
+ export async function getComplianceActionItems(
90
+ db: Database,
91
+ input: ActionItemsInput
92
+ ): Promise<ActionItemsResult> {
93
+ const { regulation, sections } = input;
94
+
95
+ if (!regulation) {
96
+ throw new Error('Regulation is required');
97
+ }
98
+
99
+ if (!sections || sections.length === 0) {
100
+ throw new Error('At least one section must be specified');
101
+ }
102
+
103
+ if (sections.length > 20) {
104
+ throw new Error('Cannot process more than 20 sections at once');
105
+ }
106
+
107
+ // Fetch section data
108
+ const actionItems: ActionItem[] = [];
109
+
110
+ for (const sectionNumber of sections) {
111
+ const sectionData = await getSection(db, {
112
+ regulation,
113
+ section: sectionNumber,
114
+ });
115
+
116
+ if (!sectionData) {
117
+ console.warn(`Section ${sectionNumber} not found in ${regulation}, skipping`);
118
+ continue;
119
+ }
120
+
121
+ // Extract action item components
122
+ const priority = extractPriority(sectionData.text);
123
+ const evidenceNeeded = extractEvidenceNeeded(sectionData.text);
124
+
125
+ // Generate required state description
126
+ const requiredState = generateRequiredState(sectionData);
127
+
128
+ actionItems.push({
129
+ section: `§${sectionData.section_number}`,
130
+ title: sectionData.title || `Section ${sectionData.section_number}`,
131
+ required_state: requiredState,
132
+ priority,
133
+ evidence_needed: evidenceNeeded.length > 0 ? evidenceNeeded : ['supporting documentation'],
134
+ });
135
+ }
136
+
137
+ return {
138
+ regulation,
139
+ action_items: actionItems,
140
+ total_items: actionItems.length,
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Generate a concise required state description from section data.
146
+ * Extracts first sentence or up to 200 characters from section text.
147
+ */
148
+ function generateRequiredState(section: SectionData): string {
149
+ const text = section.text.trim();
150
+
151
+ // Try to extract first sentence
152
+ const firstSentence = text.match(/^[^.!?]+[.!?]/);
153
+ if (firstSentence) {
154
+ let sentence = firstSentence[0].trim();
155
+ if (sentence.length > 200) {
156
+ sentence = sentence.substring(0, 197) + '...';
157
+ }
158
+ return `Comply with requirements of §${section.section_number}: ${sentence}`;
159
+ }
160
+
161
+ // Fallback: first 200 characters
162
+ const truncated = text.length > 200 ? text.substring(0, 197) + '...' : text;
163
+ return `Comply with requirements of §${section.section_number}: ${truncated}`;
164
+ }
@@ -0,0 +1,91 @@
1
+ import type { Database } from 'better-sqlite3';
2
+
3
+ export interface ApplicabilityInput {
4
+ sector: string;
5
+ subsector?: string;
6
+ }
7
+
8
+ export interface ApplicabilityRule {
9
+ regulation: string;
10
+ sector: string;
11
+ subsector: string | null;
12
+ applies: boolean;
13
+ confidence: 'definite' | 'likely' | 'possible';
14
+ basis_section: string | null;
15
+ notes: string | null;
16
+ }
17
+
18
+ export interface ApplicabilityResult {
19
+ sector: string;
20
+ subsector: string | null;
21
+ applicable_regulations: ApplicabilityRule[];
22
+ total_applicable: number;
23
+ }
24
+
25
+ /**
26
+ * Check which regulations apply to a specific sector/subsector.
27
+ * Returns regulations that apply with confidence levels.
28
+ */
29
+ export async function checkApplicability(
30
+ db: Database,
31
+ input: ApplicabilityInput
32
+ ): Promise<ApplicabilityResult> {
33
+ const { sector, subsector } = input;
34
+
35
+ if (!sector || sector.trim().length === 0) {
36
+ throw new Error('Sector is required (e.g., "healthcare", "financial", "retail")');
37
+ }
38
+
39
+ // Query applicability rules
40
+ let sql = `
41
+ SELECT
42
+ regulation,
43
+ sector,
44
+ subsector,
45
+ applies,
46
+ confidence,
47
+ basis_section,
48
+ notes
49
+ FROM applicability_rules
50
+ WHERE sector = ?
51
+ `;
52
+
53
+ const params: string[] = [sector];
54
+
55
+ if (subsector) {
56
+ sql += ' AND (subsector = ? OR subsector IS NULL)';
57
+ params.push(subsector);
58
+ }
59
+
60
+ sql += ' ORDER BY applies DESC, confidence ASC';
61
+
62
+ const rows = db.prepare(sql).all(...params) as Array<{
63
+ regulation: string;
64
+ sector: string;
65
+ subsector: string | null;
66
+ applies: number; // Database stores as INTEGER (0 or 1)
67
+ confidence: 'definite' | 'likely' | 'possible';
68
+ basis_section: string | null;
69
+ notes: string | null;
70
+ }>;
71
+
72
+ // Filter to only regulations that apply
73
+ const applicableRules = rows
74
+ .filter((row) => row.applies === 1)
75
+ .map((row) => ({
76
+ regulation: row.regulation,
77
+ sector: row.sector,
78
+ subsector: row.subsector,
79
+ applies: row.applies === 1,
80
+ confidence: row.confidence,
81
+ basis_section: row.basis_section,
82
+ notes: row.notes,
83
+ }));
84
+
85
+ return {
86
+ sector,
87
+ subsector: subsector || null,
88
+ applicable_regulations: applicableRules,
89
+ total_applicable: applicableRules.length,
90
+ };
91
+ }
@@ -0,0 +1,61 @@
1
+ import type { Database } from 'better-sqlite3';
2
+ import { searchRegulations, SearchResult } from './search.js';
3
+
4
+ export interface CompareInput {
5
+ topic: string;
6
+ regulations: string[];
7
+ }
8
+
9
+ export interface CompareResult {
10
+ topic: string;
11
+ comparisons: Array<{
12
+ regulation: string;
13
+ matches: SearchResult[];
14
+ total_matches: number;
15
+ }>;
16
+ }
17
+
18
+ /**
19
+ * Compare requirements across multiple regulations for a specific topic.
20
+ * Uses search_regulations internally to find relevant sections in each regulation.
21
+ */
22
+ export async function compareRequirements(
23
+ db: Database,
24
+ input: CompareInput
25
+ ): Promise<CompareResult> {
26
+ const { topic, regulations } = input;
27
+
28
+ if (!topic || topic.trim().length === 0) {
29
+ throw new Error('Topic is required');
30
+ }
31
+
32
+ if (!regulations || regulations.length === 0) {
33
+ throw new Error('At least one regulation must be specified');
34
+ }
35
+
36
+ if (regulations.length > 10) {
37
+ throw new Error('Cannot compare more than 10 regulations at once');
38
+ }
39
+
40
+ // Search each regulation independently
41
+ const comparisons = await Promise.all(
42
+ regulations.map(async (regulation) => {
43
+ const results = await searchRegulations(db, {
44
+ query: topic,
45
+ regulations: [regulation],
46
+ limit: 5, // Top 5 matches per regulation
47
+ });
48
+
49
+ return {
50
+ regulation,
51
+ matches: results,
52
+ total_matches: results.length,
53
+ };
54
+ })
55
+ );
56
+
57
+ return {
58
+ topic,
59
+ comparisons,
60
+ };
61
+ }
@@ -0,0 +1,79 @@
1
+ import type { Database } from 'better-sqlite3';
2
+
3
+ export interface DefinitionsInput {
4
+ term: string;
5
+ regulation?: string;
6
+ }
7
+
8
+ export interface Definition {
9
+ regulation: string;
10
+ term: string;
11
+ definition: string;
12
+ section: string;
13
+ }
14
+
15
+ export interface DefinitionsResult {
16
+ term: string;
17
+ definitions: Definition[];
18
+ total_definitions: number;
19
+ }
20
+
21
+ function escapeSqlLike(str: string): string {
22
+ return str.replace(/[%_]/g, '\\$&');
23
+ }
24
+
25
+ /**
26
+ * Look up official term definitions across regulations.
27
+ * Uses LIKE search to match partial terms (e.g., "health" matches "protected health information").
28
+ */
29
+ export async function getDefinitions(
30
+ db: Database,
31
+ input: DefinitionsInput
32
+ ): Promise<DefinitionsResult> {
33
+ const { term, regulation } = input;
34
+
35
+ if (!term || term.trim().length === 0) {
36
+ throw new Error('Term is required');
37
+ }
38
+
39
+ // Build query with optional regulation filter
40
+ let sql = `
41
+ SELECT
42
+ regulation,
43
+ term,
44
+ definition,
45
+ section
46
+ FROM definitions
47
+ WHERE term LIKE ?
48
+ `;
49
+
50
+ // Use LIKE for partial matching (case-insensitive)
51
+ const params: string[] = [`%${escapeSqlLike(term)}%`];
52
+
53
+ if (regulation) {
54
+ sql += ' AND regulation = ?';
55
+ params.push(regulation);
56
+ }
57
+
58
+ sql += ' ORDER BY regulation, term';
59
+
60
+ const rows = db.prepare(sql).all(...params) as Array<{
61
+ regulation: string;
62
+ term: string;
63
+ definition: string;
64
+ section: string;
65
+ }>;
66
+
67
+ const definitions = rows.map((row) => ({
68
+ regulation: row.regulation,
69
+ term: row.term,
70
+ definition: row.definition,
71
+ section: row.section,
72
+ }));
73
+
74
+ return {
75
+ term,
76
+ definitions,
77
+ total_definitions: definitions.length,
78
+ };
79
+ }
@@ -0,0 +1,53 @@
1
+ import type { Database } from 'better-sqlite3';
2
+
3
+ export interface EvidenceInput {
4
+ regulation: string;
5
+ section: string;
6
+ }
7
+
8
+ export interface EvidenceRequirement {
9
+ evidence_type: string;
10
+ description: string;
11
+ required: boolean;
12
+ }
13
+
14
+ export interface EvidenceResult {
15
+ regulation: string;
16
+ section: string;
17
+ evidence_requirements: EvidenceRequirement[];
18
+ notes: string;
19
+ }
20
+
21
+ /**
22
+ * Get evidence requirements for compliance with a specific section.
23
+ * MVP: Returns placeholder until evidence data is seeded.
24
+ * Future: Will query evidence_requirements table.
25
+ */
26
+ export async function getEvidenceRequirements(
27
+ db: Database,
28
+ input: EvidenceInput
29
+ ): Promise<EvidenceResult> {
30
+ const { regulation, section } = input;
31
+
32
+ if (!regulation || !section) {
33
+ throw new Error('Both regulation and section are required');
34
+ }
35
+
36
+ // Verify section exists
37
+ const sectionExists = db
38
+ .prepare('SELECT 1 FROM sections WHERE regulation = ? AND section_number = ?')
39
+ .get(regulation, section);
40
+
41
+ if (!sectionExists) {
42
+ throw new Error(`Section ${section} not found in regulation ${regulation}`);
43
+ }
44
+
45
+ // MVP: Return empty array with note about future implementation
46
+ // In production, this would query an evidence_requirements table
47
+ return {
48
+ regulation,
49
+ section,
50
+ evidence_requirements: [],
51
+ notes: 'Evidence requirements data not yet available. This feature will be implemented in a future release with specific audit artifacts, policies, and procedures required for compliance.',
52
+ };
53
+ }
@@ -0,0 +1,120 @@
1
+ import type { Database } from 'better-sqlite3';
2
+
3
+ export interface ListInput {
4
+ regulation?: string;
5
+ }
6
+
7
+ export interface RegulationInfo {
8
+ id: string;
9
+ full_name: string;
10
+ citation: string;
11
+ effective_date: string | null;
12
+ jurisdiction: string | null;
13
+ }
14
+
15
+ export interface Chapter {
16
+ name: string;
17
+ sections: string[];
18
+ }
19
+
20
+ export interface RegulationStructure {
21
+ regulation: RegulationInfo;
22
+ chapters: Chapter[];
23
+ }
24
+
25
+ export interface ListResult {
26
+ regulations?: RegulationInfo[];
27
+ structure?: RegulationStructure;
28
+ }
29
+
30
+ export async function listRegulations(
31
+ db: Database,
32
+ input: ListInput
33
+ ): Promise<ListResult> {
34
+ const { regulation } = input;
35
+
36
+ if (regulation) {
37
+ // Get specific regulation with chapters
38
+ const regRow = db.prepare(`
39
+ SELECT id, full_name, citation, effective_date, jurisdiction
40
+ FROM regulations
41
+ WHERE id = ?
42
+ `).get(regulation) as {
43
+ id: string;
44
+ full_name: string;
45
+ citation: string;
46
+ effective_date: string | null;
47
+ jurisdiction: string | null;
48
+ } | undefined;
49
+
50
+ if (!regRow) {
51
+ return { regulations: [] };
52
+ }
53
+
54
+ // Get sections grouped by chapter
55
+ const sections = db.prepare(`
56
+ SELECT section_number, title, chapter
57
+ FROM sections
58
+ WHERE regulation = ?
59
+ ORDER BY section_number
60
+ `).all(regulation) as Array<{
61
+ section_number: string;
62
+ title: string | null;
63
+ chapter: string | null;
64
+ }>;
65
+
66
+ // Group by chapter
67
+ const chapterMap = new Map<string, Chapter>();
68
+ for (const section of sections) {
69
+ const chapterKey = section.chapter || 'General';
70
+ if (!chapterMap.has(chapterKey)) {
71
+ chapterMap.set(chapterKey, {
72
+ name: chapterKey,
73
+ sections: [],
74
+ });
75
+ }
76
+ chapterMap.get(chapterKey)!.sections.push(section.section_number);
77
+ }
78
+
79
+ return {
80
+ structure: {
81
+ regulation: {
82
+ id: regRow.id,
83
+ full_name: regRow.full_name,
84
+ citation: regRow.citation,
85
+ effective_date: regRow.effective_date,
86
+ jurisdiction: regRow.jurisdiction,
87
+ },
88
+ chapters: Array.from(chapterMap.values()),
89
+ },
90
+ };
91
+ }
92
+
93
+ // List all regulations
94
+ const rows = db.prepare(`
95
+ SELECT
96
+ id,
97
+ full_name,
98
+ citation,
99
+ effective_date,
100
+ jurisdiction
101
+ FROM regulations
102
+ ORDER BY id
103
+ `).all() as Array<{
104
+ id: string;
105
+ full_name: string;
106
+ citation: string;
107
+ effective_date: string | null;
108
+ jurisdiction: string | null;
109
+ }>;
110
+
111
+ return {
112
+ regulations: rows.map(row => ({
113
+ id: row.id,
114
+ full_name: row.full_name,
115
+ citation: row.citation,
116
+ effective_date: row.effective_date,
117
+ jurisdiction: row.jurisdiction,
118
+ })),
119
+ };
120
+ }
@@ -0,0 +1,100 @@
1
+ import type { Database } from 'better-sqlite3';
2
+
3
+ export interface MapControlsInput {
4
+ framework: string;
5
+ control?: string;
6
+ regulation?: string;
7
+ }
8
+
9
+ export interface ControlMapping {
10
+ framework: string;
11
+ control_id: string;
12
+ control_name: string;
13
+ regulation: string;
14
+ sections: string[];
15
+ coverage: 'full' | 'partial' | 'related';
16
+ notes: string | null;
17
+ }
18
+
19
+ export interface MapControlsResult {
20
+ framework: string;
21
+ mappings: ControlMapping[];
22
+ total_mappings: number;
23
+ }
24
+
25
+ /**
26
+ * Map NIST controls (800-53, CSF) to regulation sections.
27
+ * Can filter by specific control ID or regulation.
28
+ */
29
+ export async function mapControls(
30
+ db: Database,
31
+ input: MapControlsInput
32
+ ): Promise<MapControlsResult> {
33
+ const { framework, control, regulation } = input;
34
+
35
+ if (!framework) {
36
+ throw new Error('Framework is required (e.g., "NIST_CSF", "NIST_800_53")');
37
+ }
38
+
39
+ // Build query with optional filters
40
+ let sql = `
41
+ SELECT
42
+ framework,
43
+ control_id,
44
+ control_name,
45
+ regulation,
46
+ sections,
47
+ coverage,
48
+ notes
49
+ FROM control_mappings
50
+ WHERE framework = ?
51
+ `;
52
+
53
+ const params: string[] = [framework];
54
+
55
+ if (control) {
56
+ sql += ' AND control_id = ?';
57
+ params.push(control);
58
+ }
59
+
60
+ if (regulation) {
61
+ sql += ' AND regulation = ?';
62
+ params.push(regulation);
63
+ }
64
+
65
+ sql += ' ORDER BY control_id, regulation';
66
+
67
+ const rows = db.prepare(sql).all(...params) as Array<{
68
+ framework: string;
69
+ control_id: string;
70
+ control_name: string;
71
+ regulation: string;
72
+ sections: string;
73
+ coverage: 'full' | 'partial' | 'related';
74
+ notes: string | null;
75
+ }>;
76
+
77
+ // Parse sections JSON
78
+ const mappings = rows.map((row) => ({
79
+ framework: row.framework,
80
+ control_id: row.control_id,
81
+ control_name: row.control_name,
82
+ regulation: row.regulation,
83
+ sections: (() => {
84
+ try {
85
+ return JSON.parse(row.sections);
86
+ } catch {
87
+ console.warn(`Invalid sections JSON for ${row.control_id}`);
88
+ return [];
89
+ }
90
+ })(),
91
+ coverage: row.coverage,
92
+ notes: row.notes,
93
+ }));
94
+
95
+ return {
96
+ framework,
97
+ mappings,
98
+ total_mappings: mappings.length,
99
+ };
100
+ }