gitmodelmujtaba commited on
Commit
f3ab943
Β·
verified Β·
1 Parent(s): e5063e0

Deploy ReguAI: Neuro-Symbolic AI GRC & Automated Conformity Assessment Engine

Browse files
app.py CHANGED
@@ -50,32 +50,32 @@ def on_domain_change(selected_domain_name: str):
50
  banner = """
51
  <div style="margin-top: 12px; padding: 12px 16px; border-radius: 8px; background: #f8fafc; border: 1px dashed #cbd5e1; font-size: 13px; color: #64748b; display: flex; align-items: center; gap: 8px;">
52
  <span>ℹ️</span>
53
- <span>New regulatory sector selected. Click <strong>⚑ Run Deterministic Conformity Assessment</strong> below to evaluate.</span>
54
  </div>
55
  """
56
  if not cases:
57
- return gr.update(choices=[], value=None), "", "", "<div style='padding:15px;'>No cases found.</div>", banner
58
  titles = [c["title"] for c in cases]
59
  first_title = titles[0]
60
  text = catalog.get_case_document_text(first_title)
61
  factsheet = catalog.render_factsheet_html(first_title)
62
  quick_bar = catalog.render_quick_bar(first_title)
63
- return gr.update(choices=titles, value=first_title), text, quick_bar, factsheet, banner
64
 
65
 
66
  def on_case_change(selected_case_title: str):
67
  banner = """
68
  <div style="margin-top: 12px; padding: 12px 16px; border-radius: 8px; background: #f8fafc; border: 1px dashed #cbd5e1; font-size: 13px; color: #64748b; display: flex; align-items: center; gap: 8px;">
69
  <span>ℹ️</span>
70
- <span>Case study loaded. Click <strong>⚑ Run Deterministic Conformity Assessment</strong> below to execute deterministic SHACL verification.</span>
71
  </div>
72
  """
73
  if not selected_case_title:
74
- return "", "", "<div style='padding:15px;'>Select a case study.</div>", banner
75
  text = catalog.get_case_document_text(selected_case_title)
76
  factsheet = catalog.render_factsheet_html(selected_case_title)
77
  quick_bar = catalog.render_quick_bar(selected_case_title)
78
- return text, quick_bar, factsheet, banner
79
 
80
 
81
  def compute_assessment_data(doc_text: str, auditor_id: str, annual_turnover: float = 50000000.0, is_sme: bool = False):
@@ -616,7 +616,7 @@ div[role="tablist"] {
616
 
617
  button[role="tab"] {
618
  font-family: 'Inter', sans-serif !important;
619
- font-size: 13px !important;
620
  font-weight: 600 !important;
621
  padding: 8px 16px !important;
622
  border-radius: 8px 8px 0 0 !important;
@@ -626,6 +626,14 @@ button[role="tab"] {
626
  transition: all 0.15s ease !important;
627
  }
628
 
 
 
 
 
 
 
 
 
629
  .dark button[role="tab"] {
630
  color: #94a3b8 !important;
631
  }
@@ -985,12 +993,19 @@ with gr.Blocks(title="ReguAI: Neuro-Symbolic AI GRC Engine") as demo:
985
  label="Statutory Summary",
986
  )
987
 
988
- spec_input = gr.Textbox(
989
- label="System Technical Specification (Markdown or JSON - Fully Editable)",
990
- lines=14,
991
- placeholder="Paste AI system architecture or model card text...",
992
- value=DEFAULT_SPEC_TEXT,
993
- )
 
 
 
 
 
 
 
994
 
995
  with gr.Accordion("βš™οΈ Corporate Exposure & Auditor Settings (Optional)", open=False):
996
  with gr.Row():
@@ -1032,13 +1047,13 @@ with gr.Blocks(title="ReguAI: Neuro-Symbolic AI GRC Engine") as demo:
1032
  # =============================================================
1033
  with gr.TabItem("2️⃣ Review Grounding", id="tab_review_grounding"):
1034
  with gr.Tabs():
1035
- with gr.TabItem("πŸ•ΈοΈ Knowledge Graph & Ontology"):
1036
- graph_output = gr.HTML(value=DEFAULT_ASSESSMENT["graph_iframe_html"], label="Force-Directed Knowledge Graph")
1037
  with gr.TabItem("πŸ“š EUR-Lex Legal Factsheet"):
1038
  factsheet_box = gr.HTML(
1039
  value=DEFAULT_FACTSHEET,
1040
  label="Regulatory Factsheet & Provenance",
1041
  )
 
 
1042
  with gr.TabItem("πŸ” Extracted Claims"):
1043
  claims_table = gr.Dataframe(
1044
  headers=["Claim ID", "Category", "Status", "Confidence", "Target Article", "Evidence Span"],
@@ -1141,12 +1156,12 @@ with gr.Blocks(title="ReguAI: Neuro-Symbolic AI GRC Engine") as demo:
1141
  domain_dropdown.change(
1142
  fn=on_domain_change,
1143
  inputs=[domain_dropdown],
1144
- outputs=[case_dropdown, spec_input, quick_bar_box, factsheet_box, status_banner_box],
1145
  )
1146
  case_dropdown.change(
1147
  fn=on_case_change,
1148
  inputs=[case_dropdown],
1149
- outputs=[spec_input, quick_bar_box, factsheet_box, status_banner_box],
1150
  )
1151
 
1152
  # Wire assessment buttons (Tab 1 primary to Step 2, direct to Step 3, and Tab 3 re-run)
 
50
  banner = """
51
  <div style="margin-top: 12px; padding: 12px 16px; border-radius: 8px; background: #f8fafc; border: 1px dashed #cbd5e1; font-size: 13px; color: #64748b; display: flex; align-items: center; gap: 8px;">
52
  <span>ℹ️</span>
53
+ <span>New regulatory sector selected. Click <strong>⚑ Run Assessment & Review Grounding (Step 2) βž”</strong> below to evaluate.</span>
54
  </div>
55
  """
56
  if not cases:
57
+ return gr.update(choices=[], value=None), "", "", "<div style='padding:15px;'>No cases found.</div>", "<div style='padding:15px;'>No cases found.</div>", banner
58
  titles = [c["title"] for c in cases]
59
  first_title = titles[0]
60
  text = catalog.get_case_document_text(first_title)
61
  factsheet = catalog.render_factsheet_html(first_title)
62
  quick_bar = catalog.render_quick_bar(first_title)
63
+ return gr.update(choices=titles, value=first_title), text, quick_bar, factsheet, factsheet, banner
64
 
65
 
66
  def on_case_change(selected_case_title: str):
67
  banner = """
68
  <div style="margin-top: 12px; padding: 12px 16px; border-radius: 8px; background: #f8fafc; border: 1px dashed #cbd5e1; font-size: 13px; color: #64748b; display: flex; align-items: center; gap: 8px;">
69
  <span>ℹ️</span>
70
+ <span>Case study loaded. Click <strong>⚑ Run Assessment & Review Grounding (Step 2) βž”</strong> below to execute deterministic SHACL verification.</span>
71
  </div>
72
  """
73
  if not selected_case_title:
74
+ return "", "", "<div style='padding:15px;'>Select a case study.</div>", "<div style='padding:15px;'>Select a case study.</div>", banner
75
  text = catalog.get_case_document_text(selected_case_title)
76
  factsheet = catalog.render_factsheet_html(selected_case_title)
77
  quick_bar = catalog.render_quick_bar(selected_case_title)
78
+ return text, quick_bar, factsheet, factsheet, banner
79
 
80
 
81
  def compute_assessment_data(doc_text: str, auditor_id: str, annual_turnover: float = 50000000.0, is_sme: bool = False):
 
616
 
617
  button[role="tab"] {
618
  font-family: 'Inter', sans-serif !important;
619
+ font-size: 13.5px !important;
620
  font-weight: 600 !important;
621
  padding: 8px 16px !important;
622
  border-radius: 8px 8px 0 0 !important;
 
626
  transition: all 0.15s ease !important;
627
  }
628
 
629
+ .factsheet-container, .factsheet-container * {
630
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
631
+ }
632
+
633
+ .factsheet-container code, .factsheet-container pre {
634
+ font-family: 'JetBrains Mono', monospace !important;
635
+ }
636
+
637
  .dark button[role="tab"] {
638
  color: #94a3b8 !important;
639
  }
 
993
  label="Statutory Summary",
994
  )
995
 
996
+ with gr.Tabs():
997
+ with gr.TabItem("πŸ“ System Technical Specification"):
998
+ spec_input = gr.Textbox(
999
+ label="System Technical Specification (Markdown or JSON - Fully Editable)",
1000
+ lines=14,
1001
+ placeholder="Paste AI system architecture or model card text...",
1002
+ value=DEFAULT_SPEC_TEXT,
1003
+ )
1004
+ with gr.TabItem("πŸ“š EUR-Lex Legal Factsheet"):
1005
+ step1_factsheet_box = gr.HTML(
1006
+ value=DEFAULT_FACTSHEET,
1007
+ label="Regulatory Factsheet & Provenance",
1008
+ )
1009
 
1010
  with gr.Accordion("βš™οΈ Corporate Exposure & Auditor Settings (Optional)", open=False):
1011
  with gr.Row():
 
1047
  # =============================================================
1048
  with gr.TabItem("2️⃣ Review Grounding", id="tab_review_grounding"):
1049
  with gr.Tabs():
 
 
1050
  with gr.TabItem("πŸ“š EUR-Lex Legal Factsheet"):
1051
  factsheet_box = gr.HTML(
1052
  value=DEFAULT_FACTSHEET,
1053
  label="Regulatory Factsheet & Provenance",
1054
  )
1055
+ with gr.TabItem("πŸ•ΈοΈ Knowledge Graph & Ontology"):
1056
+ graph_output = gr.HTML(value=DEFAULT_ASSESSMENT["graph_iframe_html"], label="Force-Directed Knowledge Graph")
1057
  with gr.TabItem("πŸ” Extracted Claims"):
1058
  claims_table = gr.Dataframe(
1059
  headers=["Claim ID", "Category", "Status", "Confidence", "Target Article", "Evidence Span"],
 
1156
  domain_dropdown.change(
1157
  fn=on_domain_change,
1158
  inputs=[domain_dropdown],
1159
+ outputs=[case_dropdown, spec_input, quick_bar_box, factsheet_box, step1_factsheet_box, status_banner_box],
1160
  )
1161
  case_dropdown.change(
1162
  fn=on_case_change,
1163
  inputs=[case_dropdown],
1164
+ outputs=[spec_input, quick_bar_box, factsheet_box, step1_factsheet_box, status_banner_box],
1165
  )
1166
 
1167
  # Wire assessment buttons (Tab 1 primary to Step 2, direct to Step 3, and Tab 3 re-run)
data/active_learning_triplets.jsonl CHANGED
@@ -24,3 +24,5 @@
24
  {"timestamp": "2026-09-21T16:35:03.024528+00:00", "auditor_id": "compliance_lead_01", "claim_id": "clm_test_99", "anchor_text": "Verified operational override in production dashboard.", "positive_label": "HUMAN_OVERSIGHT", "negative_label": "IRRELEVANT_TEXT", "verified_assertion_status": "IMPLEMENTED", "auditor_notes": "Verified operational override in production dashboard."}
25
  {"timestamp": "2026-09-21T16:44:05.202261+00:00", "auditor_id": "compliance_lead_01", "claim_id": "clm_test_99", "anchor_text": "Verified operational override in production dashboard.", "positive_label": "HUMAN_OVERSIGHT", "negative_label": "IRRELEVANT_TEXT", "verified_assertion_status": "IMPLEMENTED", "auditor_notes": "Verified operational override in production dashboard."}
26
  {"timestamp": "2026-09-21T16:49:24.911620+00:00", "auditor_id": "compliance_lead_01", "claim_id": "clm_test_99", "anchor_text": "Verified operational override in production dashboard.", "positive_label": "HUMAN_OVERSIGHT", "negative_label": "IRRELEVANT_TEXT", "verified_assertion_status": "IMPLEMENTED", "auditor_notes": "Verified operational override in production dashboard."}
 
 
 
24
  {"timestamp": "2026-09-21T16:35:03.024528+00:00", "auditor_id": "compliance_lead_01", "claim_id": "clm_test_99", "anchor_text": "Verified operational override in production dashboard.", "positive_label": "HUMAN_OVERSIGHT", "negative_label": "IRRELEVANT_TEXT", "verified_assertion_status": "IMPLEMENTED", "auditor_notes": "Verified operational override in production dashboard."}
25
  {"timestamp": "2026-09-21T16:44:05.202261+00:00", "auditor_id": "compliance_lead_01", "claim_id": "clm_test_99", "anchor_text": "Verified operational override in production dashboard.", "positive_label": "HUMAN_OVERSIGHT", "negative_label": "IRRELEVANT_TEXT", "verified_assertion_status": "IMPLEMENTED", "auditor_notes": "Verified operational override in production dashboard."}
26
  {"timestamp": "2026-09-21T16:49:24.911620+00:00", "auditor_id": "compliance_lead_01", "claim_id": "clm_test_99", "anchor_text": "Verified operational override in production dashboard.", "positive_label": "HUMAN_OVERSIGHT", "negative_label": "IRRELEVANT_TEXT", "verified_assertion_status": "IMPLEMENTED", "auditor_notes": "Verified operational override in production dashboard."}
27
+ {"timestamp": "2026-09-21T17:32:38.378505+00:00", "auditor_id": "compliance_lead_01", "claim_id": "clm_test_99", "anchor_text": "Verified operational override in production dashboard.", "positive_label": "HUMAN_OVERSIGHT", "negative_label": "IRRELEVANT_TEXT", "verified_assertion_status": "IMPLEMENTED", "auditor_notes": "Verified operational override in production dashboard."}
28
+ {"timestamp": "2026-09-21T17:34:24.366530+00:00", "auditor_id": "compliance_lead_01", "claim_id": "clm_test_99", "anchor_text": "Verified operational override in production dashboard.", "positive_label": "HUMAN_OVERSIGHT", "negative_label": "IRRELEVANT_TEXT", "verified_assertion_status": "IMPLEMENTED", "auditor_notes": "Verified operational override in production dashboard."}
src/core/case_catalog.py CHANGED
@@ -103,17 +103,17 @@ class CaseStudyCatalog:
103
  eli_uri = case.get("provenance", {}).get("eli_uri", "http://data.europa.eu/eli/reg/2024/1689/oj")
104
 
105
  return f"""
106
- <div class="quick-summary-bar" style="display:flex; flex-wrap:wrap; gap:8px; align-items:center; padding:8px 12px; background:#f8fafc; border:1px solid #e2e8f0; border-radius:6px; margin:4px 0 10px 0; font-size:12px;">
107
- <span style="background:{pill_bg}; color:{pill_color}; border:1px solid {pill_border}; padding:2px 8px; border-radius:4px; font-weight:700;">
108
  {expected}
109
  </span>
110
- <span style="background:#e0f2fe; color:#0369a1; border:1px solid #bae6fd; padding:2px 8px; border-radius:4px; font-weight:600;">
111
  βš–οΈ {tier}
112
  </span>
113
- <span style="background:#fef2f2; color:#991b1b; border:1px solid #fecaca; padding:2px 8px; border-radius:4px; font-weight:600;">
114
  πŸ’° {fine.split('(')[0].strip()}
115
  </span>
116
- <a href="{eli_uri}" target="_blank" style="margin-left:auto; color:#2563eb; text-decoration:none; font-weight:600; display:flex; align-items:center; gap:2px;">
117
  πŸ“œ EUR-Lex CELEX:{celex} β†—
118
  </a>
119
  </div>
@@ -122,7 +122,7 @@ class CaseStudyCatalog:
122
  def render_factsheet_html(self, case_id_or_title: str) -> str:
123
  case = self.get_case(case_id_or_title) or self.get_case_by_title(case_id_or_title)
124
  if not case:
125
- return "<div style='padding:15px; color:#64748b;'>Select a case study to display statutory reference factsheet.</div>"
126
 
127
  prov = case.get("provenance", {})
128
  reqs = case.get("regulatory_requirements", {})
@@ -148,93 +148,93 @@ class CaseStudyCatalog:
148
  pill_bg = "#fffbeb"
149
  pill_border = "#fde68a"
150
 
151
- # Mandatory articles tags
152
  articles_html = " ".join([
153
- f"<span style='background:#e2e8f0; color:#334155; padding:2px 8px; border-radius:4px; font-size:11px; font-weight:600;'>{a}</span>"
154
  for a in reqs.get("mandatory_articles", [])
155
  ])
156
 
157
  # NIST tags
158
  nist_html = " ".join([
159
- f"<span style='background:#dbeafe; color:#1e40af; padding:2px 6px; border-radius:4px; font-size:11px; font-weight:500;'>{n}</span>"
160
  for n in frameworks.get("nist_ai_rmf", [])
161
  ])
162
 
163
  # ISO tags
164
  iso_html = " ".join([
165
- f"<span style='background:#f3e8ff; color:#6b21a8; padding:2px 6px; border-radius:4px; font-size:11px; font-weight:500;'>{i}</span>"
166
  for i in frameworks.get("iso_42001", [])
167
  ])
168
 
169
  # GDPR tags
170
  gdpr_html = " ".join([
171
- f"<span style='background:#e0e7ff; color:#3730a3; padding:2px 6px; border-radius:4px; font-size:11px; font-weight:500;'>{g}</span>"
172
  for g in frameworks.get("gdpr", [])
173
  ])
174
 
175
  html = f"""
176
- <div style="background:#ffffff; border:1px solid #cbd5e1; border-radius:8px; padding:16px; margin-bottom:14px; box-shadow:0 1px 3px rgba(0,0,0,0.05); font-family:-apple-system,BlinkMacSystemFont,sans-serif;" class="factsheet-container">
177
- <div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:10px; flex-wrap:wrap; gap:8px;">
178
  <div>
179
- <span style="background:#0284c7; color:#ffffff; padding:3px 10px; border-radius:12px; font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:0.5px;">
180
  {case.get('domain_name', 'Domain')}
181
  </span>
182
- <h3 style="margin:6px 0 2px 0; font-size:16px; font-weight:700; color:#0f172a;">
183
  {case.get('title')}
184
  </h3>
185
- <div style="font-size:12px; color:#64748b;">
186
- <strong>Legal Basis:</strong> <span style="color:#2563eb;">{case.get('legal_basis')}</span>
187
  </div>
188
  </div>
189
  <div>
190
- <span style="background:{pill_bg}; color:{pill_color}; border:1px solid {pill_border}; padding:4px 12px; border-radius:6px; font-size:12px; font-weight:700;">
191
  {expected}
192
  </span>
193
  </div>
194
  </div>
195
 
196
- <div style="background:#f8fafc; border-left:4px solid #3b82f6; padding:10px 12px; border-radius:4px; margin-bottom:12px; font-size:13px; color:#334155; line-height:1.5;">
197
- <strong>🎯 Statutory Intended Purpose:</strong> {guidance.get('intended_purpose', '')}
198
  </div>
199
 
200
- <div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap:12px; margin-bottom:12px;">
201
- <div style="background:#f8fafc; border:1px solid #e2e8f0; border-radius:6px; padding:10px;">
202
- <div style="font-size:11px; font-weight:700; color:#64748b; text-transform:uppercase; margin-bottom:4px;">Mandatory EU AI Act Requirements</div>
203
- <div style="display:flex; flex-wrap:wrap; gap:4px; margin-top:4px;">{articles_html or 'N/A'}</div>
204
- <div style="margin-top:8px; font-size:11px; color:#475569;">
205
- <strong>Conformity Procedure:</strong> {reqs.get('conformity_procedure', 'N/A')}
206
  </div>
207
- <div style="margin-top:4px; font-size:11px; color:#b91c1c; font-weight:600;">
208
  <strong>Statutory Fine Ceiling:</strong> {reqs.get('fine_exposure_tier', 'N/A')}
209
  </div>
210
  </div>
211
 
212
- <div style="background:#f8fafc; border:1px solid #e2e8f0; border-radius:6px; padding:10px;">
213
- <div style="font-size:11px; font-weight:700; color:#64748b; text-transform:uppercase; margin-bottom:4px;">Harmonized Framework Crosswalk</div>
214
- <div style="margin-bottom:4px; font-size:11px;"><strong>NIST AI RMF:</strong> {nist_html or 'None'}</div>
215
- <div style="margin-bottom:4px; font-size:11px;"><strong>ISO/IEC 42001:</strong> {iso_html or 'None'}</div>
216
- <div style="font-size:11px;"><strong>GDPR:</strong> {gdpr_html or 'None'}</div>
217
  </div>
218
  </div>
219
 
220
- <div style="background:#fffbeb; border:1px solid #fde68a; border-radius:6px; padding:10px 12px; margin-bottom:12px; font-size:12px; color:#92400e; line-height:1.5;">
221
  <strong>⚠️ Auditor Pitfall & Common Failure Mode:</strong> {guidance.get('common_pitfalls', '')}
222
  </div>
223
 
224
  <!-- Cryptographic Provenance Block -->
225
- <div style="background:#0f172a; color:#cbd5e1; border-radius:6px; padding:12px; font-size:11px; line-height:1.6;">
226
- <div style="display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid #334155; padding-bottom:6px; margin-bottom:6px;">
227
- <span style="font-weight:700; color:#38bdf8; display:flex; align-items:center; gap:4px;">
228
  πŸ” Cryptographic Provenance & Legal Anchor (EUR-Lex)
229
  </span>
230
- <a href="{prov.get('eli_uri', 'http://data.europa.eu/eli/reg/2024/1689/oj')}" target="_blank" style="color:#60a5fa; text-decoration:none; font-weight:600;">
231
  CELEX:{prov.get('celex', '32024R1689')} β†—
232
  </a>
233
  </div>
234
- <div style="font-style:italic; color:#94a3b8; margin-bottom:6px; font-size:11px; border-left:2px solid #38bdf8; padding-left:8px;">
235
  "{prov.get('statutory_quote', '')[:220]}..."
236
  </div>
237
- <div style="display:grid; grid-template-columns:1fr 1fr; gap:6px; font-family:monospace; font-size:10px;">
238
  <div><span style="color:#64748b;">Statutory Quote SHA-256:</span> <span style="color:#a7f3d0;">{prov.get('statutory_quote_sha256', '')[:16]}...</span></div>
239
  <div><span style="color:#64748b;">Model Spec SHA-256:</span> <span style="color:#a7f3d0;">{prov.get('spec_file_sha256', '')[:16]}...</span></div>
240
  <div style="grid-column: span 2;"><span style="color:#64748b;">W3C PROV-O Entity:</span> <span style="color:#bae6fd;">{prov.get('prov_o_entity', '')}</span></div>
 
103
  eli_uri = case.get("provenance", {}).get("eli_uri", "http://data.europa.eu/eli/reg/2024/1689/oj")
104
 
105
  return f"""
106
+ <div class="quick-summary-bar" style="display:flex; flex-wrap:wrap; gap:8px; align-items:center; padding:9px 14px; background:#f8fafc; border:1px solid #e2e8f0; border-radius:8px; margin:4px 0 12px 0; font-family:'Inter', sans-serif; font-size:13px;">
107
+ <span style="background:{pill_bg}; color:{pill_color}; border:1px solid {pill_border}; padding:3px 10px; border-radius:6px; font-weight:700; font-size:12.5px;">
108
  {expected}
109
  </span>
110
+ <span style="background:#e0f2fe; color:#0369a1; border:1px solid #bae6fd; padding:3px 10px; border-radius:6px; font-weight:600; font-size:12.5px;">
111
  βš–οΈ {tier}
112
  </span>
113
+ <span style="background:#fef2f2; color:#991b1b; border:1px solid #fecaca; padding:3px 10px; border-radius:6px; font-weight:600; font-size:12.5px;">
114
  πŸ’° {fine.split('(')[0].strip()}
115
  </span>
116
+ <a href="{eli_uri}" target="_blank" style="margin-left:auto; color:#2563eb; text-decoration:none; font-weight:600; font-size:13px; display:flex; align-items:center; gap:4px;">
117
  πŸ“œ EUR-Lex CELEX:{celex} β†—
118
  </a>
119
  </div>
 
122
  def render_factsheet_html(self, case_id_or_title: str) -> str:
123
  case = self.get_case(case_id_or_title) or self.get_case_by_title(case_id_or_title)
124
  if not case:
125
+ return "<div style=\"padding:16px; color:#64748b; font-family:'Inter', sans-serif; font-size:13.5px;\">Select a case study to display statutory reference factsheet.</div>"
126
 
127
  prov = case.get("provenance", {})
128
  reqs = case.get("regulatory_requirements", {})
 
148
  pill_bg = "#fffbeb"
149
  pill_border = "#fde68a"
150
 
151
+ # Mandatory articles tags (12.5px symmetric pills)
152
  articles_html = " ".join([
153
+ f"<span style='background:#e2e8f0; color:#1e293b; padding:3px 9px; border-radius:5px; font-size:12.5px; font-weight:600;'>{a}</span>"
154
  for a in reqs.get("mandatory_articles", [])
155
  ])
156
 
157
  # NIST tags
158
  nist_html = " ".join([
159
+ f"<span style='background:#dbeafe; color:#1e40af; padding:3px 8px; border-radius:5px; font-size:12.5px; font-weight:500;'>{n}</span>"
160
  for n in frameworks.get("nist_ai_rmf", [])
161
  ])
162
 
163
  # ISO tags
164
  iso_html = " ".join([
165
+ f"<span style='background:#f3e8ff; color:#6b21a8; padding:3px 8px; border-radius:5px; font-size:12.5px; font-weight:500;'>{i}</span>"
166
  for i in frameworks.get("iso_42001", [])
167
  ])
168
 
169
  # GDPR tags
170
  gdpr_html = " ".join([
171
+ f"<span style='background:#e0e7ff; color:#3730a3; padding:3px 8px; border-radius:5px; font-size:12.5px; font-weight:500;'>{g}</span>"
172
  for g in frameworks.get("gdpr", [])
173
  ])
174
 
175
  html = f"""
176
+ <div style="background:#ffffff; border:1px solid #cbd5e1; border-radius:10px; padding:20px; margin-bottom:16px; box-shadow:0 1px 4px rgba(0,0,0,0.04); font-family:'Inter', -apple-system, BlinkMacSystemFont, sans-serif; font-size:13.5px; line-height:1.6;" class="factsheet-container">
177
+ <div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:14px; flex-wrap:wrap; gap:10px;">
178
  <div>
179
+ <span style="background:#0284c7; color:#ffffff; padding:4px 11px; border-radius:12px; font-size:12px; font-weight:700; text-transform:uppercase; letter-spacing:0.5px;">
180
  {case.get('domain_name', 'Domain')}
181
  </span>
182
+ <h3 style="margin:8px 0 4px 0; font-size:17px; font-weight:700; color:#0f172a; line-height:1.3;">
183
  {case.get('title')}
184
  </h3>
185
+ <div style="font-size:13px; color:#64748b;">
186
+ <strong>Legal Basis:</strong> <span style="color:#2563eb; font-weight:500;">{case.get('legal_basis')}</span>
187
  </div>
188
  </div>
189
  <div>
190
+ <span style="background:{pill_bg}; color:{pill_color}; border:1px solid {pill_border}; padding:5px 14px; border-radius:6px; font-size:13px; font-weight:700;">
191
  {expected}
192
  </span>
193
  </div>
194
  </div>
195
 
196
+ <div style="background:#f8fafc; border-left:4px solid #3b82f6; padding:12px 16px; border-radius:6px; margin-bottom:14px; font-size:13.5px; color:#334155; line-height:1.6;">
197
+ <strong style="color:#0f172a;">🎯 Statutory Intended Purpose:</strong> {guidance.get('intended_purpose', '')}
198
  </div>
199
 
200
+ <div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap:14px; margin-bottom:14px;">
201
+ <div style="background:#f8fafc; border:1px solid #e2e8f0; border-radius:8px; padding:14px;">
202
+ <div style="font-size:12px; font-weight:700; color:#475569; text-transform:uppercase; letter-spacing:0.5px; margin-bottom:8px;">Mandatory EU AI Act Requirements</div>
203
+ <div style="display:flex; flex-wrap:wrap; gap:6px; margin-top:6px;">{articles_html or 'N/A'}</div>
204
+ <div style="margin-top:10px; font-size:13px; color:#334155; line-height:1.5;">
205
+ <strong style="color:#0f172a;">Conformity Procedure:</strong> {reqs.get('conformity_procedure', 'N/A')}
206
  </div>
207
+ <div style="margin-top:6px; font-size:13px; color:#b91c1c; font-weight:600; line-height:1.5;">
208
  <strong>Statutory Fine Ceiling:</strong> {reqs.get('fine_exposure_tier', 'N/A')}
209
  </div>
210
  </div>
211
 
212
+ <div style="background:#f8fafc; border:1px solid #e2e8f0; border-radius:8px; padding:14px;">
213
+ <div style="font-size:12px; font-weight:700; color:#475569; text-transform:uppercase; letter-spacing:0.5px; margin-bottom:8px;">Harmonized Framework Crosswalk</div>
214
+ <div style="margin-bottom:6px; font-size:13px; color:#334155;"><strong style="color:#0f172a;">NIST AI RMF:</strong> {nist_html or 'None'}</div>
215
+ <div style="margin-bottom:6px; font-size:13px; color:#334155;"><strong style="color:#0f172a;">ISO/IEC 42001:</strong> {iso_html or 'None'}</div>
216
+ <div style="font-size:13px; color:#334155;"><strong style="color:#0f172a;">GDPR:</strong> {gdpr_html or 'None'}</div>
217
  </div>
218
  </div>
219
 
220
+ <div style="background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:12px 16px; margin-bottom:14px; font-size:13.5px; color:#92400e; line-height:1.6;">
221
  <strong>⚠️ Auditor Pitfall & Common Failure Mode:</strong> {guidance.get('common_pitfalls', '')}
222
  </div>
223
 
224
  <!-- Cryptographic Provenance Block -->
225
+ <div style="background:#0f172a; color:#cbd5e1; border-radius:8px; padding:14px 16px; font-size:13px; line-height:1.6;">
226
+ <div style="display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid #334155; padding-bottom:8px; margin-bottom:8px;">
227
+ <span style="font-weight:700; color:#38bdf8; display:flex; align-items:center; gap:6px; font-size:13px;">
228
  πŸ” Cryptographic Provenance & Legal Anchor (EUR-Lex)
229
  </span>
230
+ <a href="{prov.get('eli_uri', 'http://data.europa.eu/eli/reg/2024/1689/oj')}" target="_blank" style="color:#60a5fa; text-decoration:none; font-weight:600; font-size:13px;">
231
  CELEX:{prov.get('celex', '32024R1689')} β†—
232
  </a>
233
  </div>
234
+ <div style="font-style:italic; color:#94a3b8; margin-bottom:10px; font-size:13px; border-left:2px solid #38bdf8; padding-left:10px; line-height:1.5;">
235
  "{prov.get('statutory_quote', '')[:220]}..."
236
  </div>
237
+ <div style="display:grid; grid-template-columns:1fr 1fr; gap:8px; font-family:'JetBrains Mono', monospace; font-size:12px;">
238
  <div><span style="color:#64748b;">Statutory Quote SHA-256:</span> <span style="color:#a7f3d0;">{prov.get('statutory_quote_sha256', '')[:16]}...</span></div>
239
  <div><span style="color:#64748b;">Model Spec SHA-256:</span> <span style="color:#a7f3d0;">{prov.get('spec_file_sha256', '')[:16]}...</span></div>
240
  <div style="grid-column: span 2;"><span style="color:#64748b;">W3C PROV-O Entity:</span> <span style="color:#bae6fd;">{prov.get('prov_o_entity', '')}</span></div>