Signet Forge 0.1.0
C++20 Parquet library with AI-native extensions
DEMO
Loading...
Searching...
No Matches
eu_ai_act_reporter.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Copyright 2026 Johnson Ogundeji
3// See LICENSE_COMMERCIAL for full terms.
4// eu_ai_act_reporter.hpp — EU AI Act Compliance Reporter
5// Phase 10d: Compliance Report Generators
6//
7// Generates compliance reports for EU Regulation 2024/1689 (EU AI Act):
8//
9// Article 12 — Record-keeping / operational logging
10// Requires automatic logging of events during operation of high-risk AI
11// systems at a level of traceability appropriate to the system's purpose.
12// For financial AI: every inference with input reference, output, confidence,
13// model version, and timestamp to nanosecond precision.
14//
15// Article 13 — Transparency and provision of information to deployers
16// Requires a transparency disclosure covering: system capabilities,
17// limitations, accuracy metrics, training data characteristics, and
18// intended purpose. Generated as a machine-readable JSON summary.
19//
20// Article 19 — Conformity assessment (simplified technical summary)
21// A summary document demonstrating: chain-of-custody integrity,
22// aggregate performance statistics, anomaly counts, and coverage period.
23// For internal QA and external audit support.
24//
25// Reads from InferenceLogWriter / DecisionLogWriter output files.
26//
27// Usage:
28// ReportOptions opts;
29// opts.system_id = "trading-ai-v2";
30// opts.start_ns = period_start;
31// opts.end_ns = period_end;
32//
33// auto art12 = EUAIActReporter::generate_article12({"inf_0.parquet"}, opts);
34// auto art13 = EUAIActReporter::generate_article13({"inf_0.parquet"}, opts);
35// auto art19 = EUAIActReporter::generate_article19(dec_files, inf_files, opts);
36
37#pragma once
38
39#if !defined(SIGNET_ENABLE_COMMERCIAL) || !SIGNET_ENABLE_COMMERCIAL
40#error "signet/ai/compliance/eu_ai_act_reporter.hpp requires SIGNET_ENABLE_COMMERCIAL=ON (AGPL-3.0 commercial tier). See LICENSE_COMMERCIAL."
41#endif
42
43#include "signet/error.hpp"
47
48#include <algorithm>
49#include <chrono>
50#include <cmath>
51#include <cstdint>
52#include <cstdio>
53#include <ctime>
54#include <numeric>
55#include <string>
56#include <vector>
57
58// Platform CSPRNG headers for random_hex_suffix_ (CR-4)
59#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__)
60# include <stdlib.h> // arc4random_buf
61#elif defined(__linux__)
62# include <sys/random.h> // getrandom
63#endif
64
65namespace signet::forge {
66
80public:
92 const std::vector<std::string>& inference_log_files,
93 const ReportOptions& opts = {}) {
94
95 auto license = commercial::require_feature("EUAIActReporter::article12");
96 if (!license) return license.error();
97
98 if (opts.format != ReportFormat::JSON)
100 "Article 12 reports currently support JSON format only"};
101
102 if (inference_log_files.empty())
103 return Error{ErrorCode::IO_ERROR,
104 "EUAIActReporter: no inference log files supplied"};
105
106 std::vector<InferenceRecord> records;
107 bool chain_ok = true;
108 std::string chain_id;
109 int read_errors = 0;
110
111 for (const auto& path : inference_log_files) {
112 auto rdr_result = InferenceLogReader::open(path);
113 if (!rdr_result) return Error{ErrorCode::IO_ERROR,
114 "EUAIActReporter: cannot open log file '" + path +
115 "': " + rdr_result.error().message};
116 auto& rdr = *rdr_result;
117
118 if (opts.verify_chain) {
119 auto vr = rdr.verify_chain();
120 if (!vr.valid) chain_ok = false;
121 }
122 if (chain_id.empty()) {
123 auto meta = rdr.audit_metadata();
124 if (meta) chain_id = meta->chain_id;
125 }
126 auto all = rdr.read_all();
127 if (!all) {
128 if (opts.strict_source_reads) {
129 return Error{ErrorCode::IO_ERROR,
130 "EUAIActReporter Art.12: failed to read '" + path +
131 "': " + all.error().message};
132 }
133 ++read_errors;
134 continue;
135 }
136 for (auto& rec : *all)
137 if (rec.timestamp_ns >= opts.start_ns &&
138 rec.timestamp_ns <= opts.end_ns)
139 records.push_back(std::move(rec));
140 }
141
142 std::sort(records.begin(), records.end(),
143 [](const InferenceRecord& a, const InferenceRecord& b){
144 return a.timestamp_ns < b.timestamp_ns; });
145
146 auto usage = commercial::record_usage_rows(
147 "EUAIActReporter::article12", static_cast<uint64_t>(records.size()));
148 if (!usage) return usage.error();
149
150 ComplianceReport report = make_report_skeleton(
152 static_cast<int64_t>(records.size()), chain_ok, chain_id);
153 if (read_errors > 0) {
154 report.incomplete_data = true;
155 report.read_errors.push_back("Art.12: failed to read " + std::to_string(read_errors)
156 + " of " + std::to_string(inference_log_files.size()) + " inference log files");
157 }
158
159 report.content = format_article12_json(records, opts, report);
160 return report;
161 }
162
174 const std::vector<std::string>& inference_log_files,
175 const ReportOptions& opts = {}) {
176
177 auto license = commercial::require_feature("EUAIActReporter::article13");
178 if (!license) return license.error();
179
180 if (inference_log_files.empty())
182 "EUAIActReporter: no inference log files supplied"};
183
184 std::vector<InferenceRecord> records;
185 bool chain_ok = true;
186 std::string chain_id;
187 int read_errors = 0;
188
189 for (const auto& path : inference_log_files) {
190 auto rdr_result = InferenceLogReader::open(path);
191 if (!rdr_result) return Error{ErrorCode::IO_ERROR,
192 "EUAIActReporter: cannot open log file '" + path +
193 "': " + rdr_result.error().message};
194 auto& rdr = *rdr_result;
195 if (opts.verify_chain) {
196 auto vr = rdr.verify_chain();
197 if (!vr.valid) chain_ok = false;
198 }
199 if (chain_id.empty()) {
200 auto meta = rdr.audit_metadata();
201 if (meta) chain_id = meta->chain_id;
202 }
203 auto all = rdr.read_all();
204 if (!all) {
205 if (opts.strict_source_reads) {
206 return Error{ErrorCode::IO_ERROR,
207 "EUAIActReporter Art.13: failed to read '" + path +
208 "': " + all.error().message};
209 }
210 ++read_errors;
211 continue;
212 }
213 for (auto& rec : *all)
214 if (rec.timestamp_ns >= opts.start_ns &&
215 rec.timestamp_ns <= opts.end_ns)
216 records.push_back(std::move(rec));
217 }
218
219 auto usage = commercial::record_usage_rows(
220 "EUAIActReporter::article13", static_cast<uint64_t>(records.size()));
221 if (!usage) return usage.error();
222
223 ComplianceReport report = make_report_skeleton(
225 static_cast<int64_t>(records.size()), chain_ok, chain_id);
226 if (read_errors > 0) {
227 report.incomplete_data = true;
228 report.read_errors.push_back("Art.13: failed to read " + std::to_string(read_errors)
229 + " of " + std::to_string(inference_log_files.size()) + " inference log files");
230 }
231
232 report.content = format_article13_json(records, opts, report);
233 return report;
234 }
235
249 const std::vector<std::string>& decision_log_files,
250 const std::vector<std::string>& inference_log_files,
251 const ReportOptions& opts = {}) {
252
253 auto license = commercial::require_feature("EUAIActReporter::article19");
254 if (!license) return license.error();
255
256 if (decision_log_files.empty() && inference_log_files.empty())
258 "EUAIActReporter: no log files supplied"};
259
260 // -- Decision records -------------------------------------------------
261 std::vector<DecisionRecord> dec_records;
262 bool dec_chain_ok = true;
263 std::string dec_chain_id;
264 int dec_read_errors = 0;
265
266 for (const auto& path : decision_log_files) {
267 auto rdr_result = DecisionLogReader::open(path);
268 if (!rdr_result) return Error{ErrorCode::IO_ERROR,
269 "EUAIActReporter: cannot open decision log '" + path +
270 "': " + rdr_result.error().message};
271 auto& rdr = *rdr_result;
272 if (opts.verify_chain) {
273 auto vr = rdr.verify_chain();
274 if (!vr.valid) dec_chain_ok = false;
275 }
276 if (dec_chain_id.empty()) {
277 auto meta = rdr.audit_metadata();
278 if (meta) dec_chain_id = meta->chain_id;
279 }
280 auto all = rdr.read_all();
281 if (!all) {
282 if (opts.strict_source_reads) {
283 return Error{ErrorCode::IO_ERROR,
284 "EUAIActReporter Art.19: failed to read decision log '" + path +
285 "': " + all.error().message};
286 }
287 ++dec_read_errors;
288 continue;
289 }
290 for (auto& rec : *all)
291 if (rec.timestamp_ns >= opts.start_ns &&
292 rec.timestamp_ns <= opts.end_ns)
293 dec_records.push_back(std::move(rec));
294 }
295
296 // -- Inference records ------------------------------------------------
297 std::vector<InferenceRecord> inf_records;
298 bool inf_chain_ok = true;
299 std::string inf_chain_id;
300 int inf_read_errors = 0;
301
302 for (const auto& path : inference_log_files) {
303 auto rdr_result = InferenceLogReader::open(path);
304 if (!rdr_result) return Error{ErrorCode::IO_ERROR,
305 "EUAIActReporter: cannot open inference log '" + path +
306 "': " + rdr_result.error().message};
307 auto& rdr = *rdr_result;
308 if (opts.verify_chain) {
309 auto vr = rdr.verify_chain();
310 if (!vr.valid) inf_chain_ok = false;
311 }
312 if (inf_chain_id.empty()) {
313 auto meta = rdr.audit_metadata();
314 if (meta) inf_chain_id = meta->chain_id;
315 }
316 auto all = rdr.read_all();
317 if (!all) {
318 if (opts.strict_source_reads) {
319 return Error{ErrorCode::IO_ERROR,
320 "EUAIActReporter Art.19: failed to read inference log '" + path +
321 "': " + all.error().message};
322 }
323 ++inf_read_errors;
324 continue;
325 }
326 for (auto& rec : *all)
327 if (rec.timestamp_ns >= opts.start_ns &&
328 rec.timestamp_ns <= opts.end_ns)
329 inf_records.push_back(std::move(rec));
330 }
331
332 // EU AI Act Art.19: cross-chain verification — when verify_chain is
333 // enabled and both log types are present, their chain IDs must match
334 // to confirm they belong to the same audit context. A mismatch means
335 // the decision and inference logs were produced by unrelated systems,
336 // which is an audit finding that must be surfaced.
337 if (opts.verify_chain &&
338 !dec_chain_id.empty() && !inf_chain_id.empty() &&
339 dec_chain_id != inf_chain_id) {
340 return Error{ErrorCode::INVALID_ARGUMENT,
341 "EU AI Act Art.19: decision chain_id ('" + dec_chain_id +
342 "') != inference chain_id ('" + inf_chain_id +
343 "'). Logs must share an audit context."};
344 }
345
346 const bool chain_ok = dec_chain_ok && inf_chain_ok;
347 const int64_t total = static_cast<int64_t>(
348 dec_records.size() + inf_records.size());
349
350 auto usage = commercial::record_usage_rows(
351 "EUAIActReporter::article19", static_cast<uint64_t>(total));
352 if (!usage) return usage.error();
353
354 ComplianceReport report = make_report_skeleton(
355 ComplianceStandard::EU_AI_ACT_ART19, opts, total, chain_ok,
356 dec_chain_id.empty() ? inf_chain_id : dec_chain_id);
357 if (dec_read_errors > 0) {
358 report.incomplete_data = true;
359 report.read_errors.push_back("Art.19: failed to read " + std::to_string(dec_read_errors)
360 + " of " + std::to_string(decision_log_files.size()) + " decision log files");
361 }
362 if (inf_read_errors > 0) {
363 report.incomplete_data = true;
364 report.read_errors.push_back("Art.19: failed to read " + std::to_string(inf_read_errors)
365 + " of " + std::to_string(inference_log_files.size()) + " inference log files");
366 }
367
368 report.content = format_article19_json(
369 dec_records, inf_records, opts, report,
370 dec_chain_ok, inf_chain_ok, dec_chain_id, inf_chain_id);
371 return report;
372 }
373
374private:
375 // =========================================================================
376 // Performance stats helper
377 // =========================================================================
378
379 struct PerfStats {
380 int64_t total = 0;
381 double avg_latency_ns = 0.0;
382 double p50_latency_ns = 0.0;
383 double p95_latency_ns = 0.0;
384 double p99_latency_ns = 0.0;
385 double avg_confidence = 0.0;
386 int64_t low_conf_count = 0;
387 int64_t anomaly_count = 0; // latency > 3σ above mean
388 int64_t total_input_tokens = 0;
389 int64_t total_output_tokens = 0;
390 int64_t total_batches = 0;
391 };
392
393 static PerfStats compute_perf(const std::vector<InferenceRecord>& recs,
394 float low_conf_thr) {
395 PerfStats s;
396 s.total = static_cast<int64_t>(recs.size());
397 if (recs.empty()) return s;
398
399 std::vector<int64_t> latencies;
400 latencies.reserve(recs.size());
401 double sum_lat = 0.0, sum_conf = 0.0;
402
403 for (const auto& r : recs) {
404 latencies.push_back(r.latency_ns);
405 sum_lat += static_cast<double>(r.latency_ns);
406 sum_conf += static_cast<double>(r.output_score);
407 if (r.output_score < low_conf_thr) ++s.low_conf_count;
408 s.total_input_tokens += r.input_tokens;
409 s.total_output_tokens += r.output_tokens;
410 s.total_batches += r.batch_size;
411 }
412
413 s.avg_latency_ns = sum_lat / static_cast<double>(recs.size());
414 s.avg_confidence = sum_conf / static_cast<double>(recs.size());
415
416 std::sort(latencies.begin(), latencies.end());
417 auto pct = [&](double p) -> double {
418 size_t idx = static_cast<size_t>(p * static_cast<double>(latencies.size() - 1));
419 return static_cast<double>(latencies[idx]);
420 };
421 s.p50_latency_ns = pct(0.50);
422 s.p95_latency_ns = pct(0.95);
423 s.p99_latency_ns = pct(0.99);
424
425 // Anomaly: latency > mean + 3σ
426 double var = 0.0;
427 for (int64_t lat : latencies) {
428 double diff = static_cast<double>(lat) - s.avg_latency_ns;
429 var += diff * diff;
430 }
431 var /= static_cast<double>(latencies.size());
432 const double sigma3 = s.avg_latency_ns + 3.0 * std::sqrt(var);
433 for (int64_t lat : latencies)
434 if (static_cast<double>(lat) > sigma3) ++s.anomaly_count;
435
436 return s;
437 }
438
439 // =========================================================================
440 // JSON formatters
441 // =========================================================================
442
443 static std::string format_article12_json(
444 const std::vector<InferenceRecord>& records,
445 const ReportOptions& opts,
446 const ComplianceReport& meta) {
447
448 const std::string ind = opts.pretty_print ? " " : "";
449 const std::string ind2 = opts.pretty_print ? " " : "";
450 const std::string nl = opts.pretty_print ? "\n" : "";
451 const std::string sp = opts.pretty_print ? " " : "";
452
453 PerfStats ps = compute_perf(records, opts.low_confidence_threshold);
454
455 std::string o;
456 o += "{" + nl;
457 o += ind + "\"report_type\":" + sp + "\"EU_AI_ACT_ARTICLE_12\"," + nl;
458 o += ind + "\"regulatory_reference\":" + sp
459 + "\"Regulation (EU) 2024/1689 — Article 12\"," + nl;
460 o += ind + "\"report_id\":" + sp + "\"" + j(meta.report_id) + "\"," + nl;
461 o += ind + "\"system_id\":" + sp
462 + "\"" + j(opts.system_id.empty() ? "UNSPECIFIED" : opts.system_id)
463 + "\"," + nl;
464 o += ind + "\"generated_at\":" + sp + "\"" + meta.generated_at_iso + "\"," + nl;
465 o += ind + "\"period_start\":" + sp + "\"" + meta.period_start_iso + "\"," + nl;
466 o += ind + "\"period_end\":" + sp + "\"" + meta.period_end_iso + "\"," + nl;
467 o += ind + "\"chain_id\":" + sp + "\"" + j(meta.chain_id) + "\"," + nl;
468 o += ind + "\"chain_verified\":" + sp + (meta.chain_verified ? "true" : "false") + "," + nl;
469 o += ind + "\"total_inferences\":" + sp + std::to_string(ps.total) + "," + nl;
470 o += ind + "\"anomaly_count\":" + sp + std::to_string(ps.anomaly_count) + "," + nl;
471 o += ind + "\"low_confidence_count\":" + sp + std::to_string(ps.low_conf_count) + "," + nl;
472 o += ind + "\"performance_summary\":" + sp + "{" + nl;
473 o += ind2 + "\"avg_latency_ns\":" + sp + dbl(ps.avg_latency_ns) + "," + nl;
474 o += ind2 + "\"p50_latency_ns\":" + sp + dbl(ps.p50_latency_ns) + "," + nl;
475 o += ind2 + "\"p95_latency_ns\":" + sp + dbl(ps.p95_latency_ns) + "," + nl;
476 o += ind2 + "\"p99_latency_ns\":" + sp + dbl(ps.p99_latency_ns) + "," + nl;
477 o += ind2 + "\"avg_output_score\":" + sp + dbl(ps.avg_confidence) + nl;
478 o += ind + "}," + nl;
479 o += ind + "\"inference_records\":" + sp + "[" + nl;
480
481 // EU AI Act Art.12(2): consistent statistical anomaly detection methodology.
482 // Precompute mean + 3*sigma threshold for per-record anomaly flag — the
483 // same formula used in compute_perf() for aggregate anomaly_count, ensuring
484 // per-record and summary anomaly classifications are always consistent.
485 double per_record_sigma3 = 0.0;
486 if (!records.empty()) {
487 double var = 0.0;
488 for (const auto& r : records) {
489 double diff = static_cast<double>(r.latency_ns) - ps.avg_latency_ns;
490 var += diff * diff;
491 }
492 var /= static_cast<double>(records.size());
493 per_record_sigma3 = ps.avg_latency_ns + 3.0 * std::sqrt(var);
494 }
495
496 for (size_t i = 0; i < records.size(); ++i) {
497 const auto& rec = records[i];
498 o += ind2 + "{" + nl;
499 o += ind2 + ind + "\"timestamp_utc\":" + sp
500 + "\"" + ns_to_iso8601(rec.timestamp_ns) + "\"," + nl;
501 o += ind2 + ind + "\"model_id\":" + sp
502 + "\"" + j(rec.model_id) + "\"," + nl;
503 o += ind2 + ind + "\"model_version\":" + sp
504 + "\"" + j(rec.model_version) + "\"," + nl;
505 o += ind2 + ind + "\"inference_type\":" + sp
506 + "\"" + inference_type_str(rec.inference_type) + "\"," + nl;
507 o += ind2 + ind + "\"input_hash\":" + sp
508 + "\"" + j(rec.input_hash) + "\"," + nl;
509 o += ind2 + ind + "\"output_hash\":" + sp
510 + "\"" + j(rec.output_hash) + "\"," + nl;
511 o += ind2 + ind + "\"output_score\":" + sp
512 + dbl(rec.output_score) + "," + nl;
513 o += ind2 + ind + "\"latency_ns\":" + sp
514 + std::to_string(rec.latency_ns) + "," + nl;
515 o += ind2 + ind + "\"batch_size\":" + sp
516 + std::to_string(rec.batch_size) + "," + nl;
517 o += ind2 + ind + "\"user_id_hash\":" + sp
518 + "\"" + j(rec.user_id_hash) + "\"," + nl;
519 // L23: per-record anomaly uses mean + 3*stddev (consistent with aggregate)
520 // Compute sigma3 threshold (same formula as compute_perf)
521 o += ind2 + ind + "\"anomaly\":" + sp
522 + (rec.latency_ns > static_cast<int64_t>(per_record_sigma3)
523 ? "true" : "false");
524 if (opts.include_features && !rec.input_embedding.empty()) {
525 o += "," + nl;
526 o += ind2 + ind + "\"input_embedding_preview\":" + sp
527 + feats_preview(rec.input_embedding, 8);
528 }
529 o += nl + ind2 + "}";
530 if (i + 1 < records.size()) o += ",";
531 o += nl;
532 }
533 o += ind + "]" + nl;
534 o += "}" + nl;
535 return o;
536 }
537
538 static std::string format_article13_json(
539 const std::vector<InferenceRecord>& records,
540 const ReportOptions& opts,
541 const ComplianceReport& meta) {
542
543 const std::string ind = opts.pretty_print ? " " : "";
544 const std::string ind2 = opts.pretty_print ? " " : "";
545 const std::string nl = opts.pretty_print ? "\n" : "";
546 const std::string sp = opts.pretty_print ? " " : "";
547
548 PerfStats ps = compute_perf(records, opts.low_confidence_threshold);
549
550 // Count inference types
551 std::array<int64_t, 8> type_counts{};
552 std::string model_versions_seen;
553 std::string last_version;
554 for (const auto& r : records) {
555 int idx = static_cast<int>(r.inference_type);
556 if (idx >= 0 && idx < 8) ++type_counts[idx];
557 if (r.model_version != last_version) {
558 if (!model_versions_seen.empty()) model_versions_seen += ", ";
559 model_versions_seen += r.model_version;
560 last_version = r.model_version;
561 }
562 }
563
564 std::string o;
565 o += "{" + nl;
566 o += ind + "\"report_type\":" + sp + "\"EU_AI_ACT_ARTICLE_13\"," + nl;
567 o += ind + "\"regulatory_reference\":" + sp
568 + "\"Regulation (EU) 2024/1689 — Article 13: Transparency\"," + nl;
569 o += ind + "\"report_id\":" + sp + "\"" + j(meta.report_id) + "\"," + nl;
570 o += ind + "\"system_id\":" + sp
571 + "\"" + j(opts.system_id.empty() ? "UNSPECIFIED" : opts.system_id) + "\"," + nl;
572 o += ind + "\"generated_at\":" + sp + "\"" + meta.generated_at_iso + "\"," + nl;
573 // EU AI Act Art.13(3) transparency disclosure (Gap R-2)
574 o += ind + "\"provider\":" + sp + "{" + nl;
575 o += ind2 + "\"name\":" + sp + "\""
576 + j(opts.provider_name.empty() ? "UNSPECIFIED" : opts.provider_name)
577 + "\"," + nl;
578 o += ind2 + "\"contact\":" + sp + "\""
579 + j(opts.provider_contact.empty() ? "UNSPECIFIED" : opts.provider_contact)
580 + "\"" + nl;
581 o += ind + "}," + nl;
582 o += ind + "\"intended_purpose\":" + sp + "\""
583 + j(opts.intended_purpose.empty()
584 ? "Not specified — Art.13(3)(b)(i) requires disclosure"
585 : opts.intended_purpose) + "\"," + nl;
586 o += ind + "\"known_limitations\":" + sp + "\""
587 + j(opts.known_limitations.empty()
588 ? "Not specified — Art.13(3)(b)(ii) requires disclosure"
589 : opts.known_limitations) + "\"," + nl;
590 o += ind + "\"instructions_for_use\":" + sp + "\""
591 + j(opts.instructions_for_use.empty()
592 ? "Not specified — Art.13(3)(b)(iv) requires disclosure"
593 : opts.instructions_for_use) + "\"," + nl;
594 o += ind + "\"human_oversight_measures\":" + sp + "\""
595 + j(opts.human_oversight_measures.empty()
596 ? "Not specified — Art.14 requires disclosure"
597 : opts.human_oversight_measures) + "\"," + nl;
598 if (!opts.accuracy_metrics.empty()) {
599 o += ind + "\"accuracy_metrics\":" + sp + "\""
600 + j(opts.accuracy_metrics) + "\"," + nl;
601 }
602 if (!opts.bias_risks.empty()) {
603 o += ind + "\"bias_risks\":" + sp + "\""
604 + j(opts.bias_risks) + "\"," + nl;
605 }
606 if (opts.risk_level > 0) {
607 o += ind + "\"risk_classification\":" + sp + "{" + nl;
608 o += ind2 + "\"level\":" + sp + std::to_string(opts.risk_level) + "," + nl;
609 const char* risk_labels[] = {"","minimal","limited","high","unacceptable"};
610 o += ind2 + "\"label\":" + sp + "\""
611 + std::string((opts.risk_level >= 0 && opts.risk_level <= 4) ? risk_labels[opts.risk_level] : "unknown")
612 + "\"" + nl;
613 o += ind + "}," + nl;
614 }
615 o += ind + "\"system_capabilities\":" + sp + "{" + nl;
616 o += ind2 + "\"supported_inference_types\":" + sp + "[";
617 const char* type_names[] = {
618 "CLASSIFICATION","REGRESSION","EMBEDDING","GENERATION",
619 "RANKING","ANOMALY","RECOMMENDATION","CUSTOM"
620 };
621 bool first = true;
622 for (int i = 0; i < 8; ++i) {
623 if (type_counts[i] > 0) {
624 if (!first) o += ",";
625 o += "\"" + std::string(type_names[i]) + "\"";
626 first = false;
627 }
628 }
629 o += "]," + nl;
630 o += ind2 + "\"model_versions_observed\":" + sp
631 + "\"" + j(model_versions_seen) + "\"," + nl;
632 o += ind2 + "\"total_inferences_in_period\":" + sp
633 + std::to_string(ps.total) + nl;
634 o += ind + "}," + nl;
635 o += ind + "\"performance_characteristics\":" + sp + "{" + nl;
636 o += ind2 + "\"latency_p50_ns\":" + sp + dbl(ps.p50_latency_ns) + "," + nl;
637 o += ind2 + "\"latency_p95_ns\":" + sp + dbl(ps.p95_latency_ns) + "," + nl;
638 o += ind2 + "\"latency_p99_ns\":" + sp + dbl(ps.p99_latency_ns) + "," + nl;
639 o += ind2 + "\"avg_output_score\":" + sp + dbl(ps.avg_confidence) + "," + nl;
640 o += ind2 + "\"low_confidence_rate\":" + sp
641 + dbl(ps.total > 0
642 ? static_cast<double>(ps.low_conf_count) / ps.total
643 : 0.0) + nl;
644 o += ind + "}," + nl;
645 o += ind + "\"data_characteristics\":" + sp + "{" + nl;
646 o += ind2 + "\"total_input_tokens\":" + sp
647 + std::to_string(ps.total_input_tokens) + "," + nl;
648 o += ind2 + "\"total_output_tokens\":" + sp
649 + std::to_string(ps.total_output_tokens) + "," + nl;
650 o += ind2 + "\"avg_batch_size\":" + sp
651 + dbl(ps.total > 0
652 ? static_cast<double>(ps.total_batches) / ps.total
653 : 0.0);
654 // EU AI Act Art.13(3)(b)(ii): training data provenance (if available)
655 {
656 std::string td_id, td_chars;
657 int64_t td_size = 0;
658 for (const auto& r : records) {
659 if (td_id.empty() && !r.training_dataset_id.empty())
660 td_id = r.training_dataset_id;
661 if (r.training_dataset_size > td_size)
662 td_size = r.training_dataset_size;
663 if (td_chars.empty() && !r.training_data_characteristics.empty())
664 td_chars = r.training_data_characteristics;
665 }
666 if (!td_id.empty() || td_size > 0 || !td_chars.empty()) {
667 o += "," + nl;
668 if (!td_id.empty())
669 o += ind2 + "\"training_dataset_id\":" + sp
670 + "\"" + j(td_id) + "\"," + nl;
671 o += ind2 + "\"training_dataset_size\":" + sp
672 + std::to_string(td_size);
673 if (!td_chars.empty()) {
674 o += "," + nl;
675 o += ind2 + "\"training_data_characteristics\":" + sp
676 + "\"" + j(td_chars) + "\"";
677 }
678 }
679 }
680 o += nl;
681 o += ind + "}," + nl;
682 o += ind + "\"limitations_and_risks\":" + sp + "{" + nl;
683 o += ind2 + "\"anomaly_count\":" + sp + std::to_string(ps.anomaly_count) + "," + nl;
684 o += ind2 + "\"chain_integrity\":" + sp
685 + (meta.chain_verified ? "\"VERIFIED\"" : "\"FAILED\"") + nl;
686 o += ind + "}" + nl;
687 o += "}" + nl;
688 return o;
689 }
690
691 static std::string format_article19_json(
692 const std::vector<DecisionRecord>& dec_recs,
693 const std::vector<InferenceRecord>& inf_recs,
694 const ReportOptions& opts,
695 const ComplianceReport& meta,
696 bool dec_chain_ok, bool inf_chain_ok,
697 const std::string& dec_chain_id,
698 const std::string& inf_chain_id) {
699
700 const std::string ind = opts.pretty_print ? " " : "";
701 const std::string ind2 = opts.pretty_print ? " " : "";
702 const std::string nl = opts.pretty_print ? "\n" : "";
703 const std::string sp = opts.pretty_print ? " " : "";
704
705 PerfStats ps = compute_perf(inf_recs, opts.low_confidence_threshold);
706
707 // Decision stats
708 int64_t orders_new = 0, orders_rejected = 0, risk_overrides = 0;
709 for (const auto& r : dec_recs) {
710 if (r.decision_type == DecisionType::ORDER_NEW) ++orders_new;
711 if (r.risk_result == RiskGateResult::REJECTED) ++orders_rejected;
712 if (r.decision_type == DecisionType::RISK_OVERRIDE)++risk_overrides;
713 }
714
715 std::string o;
716 o += "{" + nl;
717 o += ind + "\"report_type\":" + sp + "\"EU_AI_ACT_ARTICLE_19\"," + nl;
718 o += ind + "\"regulatory_reference\":" + sp
719 + "\"Regulation (EU) 2024/1689 — Article 19: Conformity Assessment\"," + nl;
720 o += ind + "\"report_id\":" + sp + "\"" + j(meta.report_id) + "\"," + nl;
721 o += ind + "\"system_id\":" + sp
722 + "\"" + j(opts.system_id.empty() ? "UNSPECIFIED" : opts.system_id) + "\"," + nl;
723 o += ind + "\"generated_at\":" + sp + "\"" + meta.generated_at_iso + "\"," + nl;
724 o += ind + "\"period_start\":" + sp + "\"" + meta.period_start_iso + "\"," + nl;
725 o += ind + "\"period_end\":" + sp + "\"" + meta.period_end_iso + "\"," + nl;
726 o += ind + "\"conformity_status\":" + sp
727 + (dec_chain_ok && inf_chain_ok ? "\"CONFORMANT\"" : "\"NON_CONFORMANT\"")
728 + "," + nl;
729 o += ind + "\"audit_trail_integrity\":" + sp + "{" + nl;
730 o += ind2 + "\"decision_chain_id\":" + sp + "\"" + j(dec_chain_id) + "\"," + nl;
731 o += ind2 + "\"decision_chain_verified\":" + sp
732 + (dec_chain_ok ? "true" : "false") + "," + nl;
733 o += ind2 + "\"inference_chain_id\":" + sp + "\"" + j(inf_chain_id) + "\"," + nl;
734 o += ind2 + "\"inference_chain_verified\":" + sp
735 + (inf_chain_ok ? "true" : "false") + nl;
736 o += ind + "}," + nl;
737 o += ind + "\"decision_statistics\":" + sp + "{" + nl;
738 o += ind2 + "\"total_decisions\":" + sp
739 + std::to_string(dec_recs.size()) + "," + nl;
740 o += ind2 + "\"orders_generated\":" + sp + std::to_string(orders_new) + "," + nl;
741 o += ind2 + "\"orders_rejected_by_risk_gate\":" + sp
742 + std::to_string(orders_rejected) + "," + nl;
743 o += ind2 + "\"risk_overrides\":" + sp + std::to_string(risk_overrides) + nl;
744 o += ind + "}," + nl;
745 o += ind + "\"inference_statistics\":" + sp + "{" + nl;
746 o += ind2 + "\"total_inferences\":" + sp + std::to_string(ps.total) + "," + nl;
747 o += ind2 + "\"avg_latency_ns\":" + sp + dbl(ps.avg_latency_ns) + "," + nl;
748 o += ind2 + "\"p99_latency_ns\":" + sp + dbl(ps.p99_latency_ns) + "," + nl;
749 o += ind2 + "\"anomaly_count\":" + sp + std::to_string(ps.anomaly_count) + "," + nl;
750 o += ind2 + "\"low_confidence_count\":" + sp
751 + std::to_string(ps.low_conf_count) + nl;
752 o += ind + "}" + nl;
753 o += "}" + nl;
754 return o;
755 }
756
757 // =========================================================================
758 // Shared utilities
759 // =========================================================================
760
761 static ComplianceReport make_report_skeleton(
763 const ReportOptions& opts,
764 int64_t total_records,
765 bool chain_ok,
766 const std::string& chain_id) {
767
768 ComplianceReport r;
769 r.standard = std_;
770 r.format = opts.format;
771 r.chain_verified = chain_ok;
772 r.chain_id = chain_id;
773 r.total_records = total_records;
774
775 using namespace std::chrono;
776 r.generated_at_ns = static_cast<int64_t>(
777 duration_cast<nanoseconds>(
778 system_clock::now().time_since_epoch()).count());
779 r.generated_at_iso = ns_to_iso8601(r.generated_at_ns);
780 r.period_start_iso = ns_to_iso8601(opts.start_ns);
781 r.period_end_iso = (opts.end_ns == (std::numeric_limits<int64_t>::max)())
782 ? "open"
783 : ns_to_iso8601(opts.end_ns);
784 r.report_id = opts.report_id.empty()
785 ? ("EUAIA-" + std::to_string(r.generated_at_ns)
786 + "-" + random_hex_suffix_())
787 : opts.report_id;
788 return r;
789 }
790
791 static std::string ns_to_iso8601(int64_t ns) {
792 static_assert(sizeof(std::time_t) >= 8,
793 "Signet compliance reporters require 64-bit time_t for timestamps beyond 2038");
794 if (ns <= 0) return "1970-01-01T00:00:00.000000000Z";
795 const int64_t secs = ns / 1'000'000'000LL;
796 const int64_t ns_part = ns % 1'000'000'000LL;
797 std::time_t t = static_cast<std::time_t>(secs);
798 std::tm tm_buf{};
799#if defined(_WIN32)
800 gmtime_s(&tm_buf, &t);
801 std::tm* utc = &tm_buf;
802#else
803 std::tm* utc = gmtime_r(&t, &tm_buf);
804#endif
805 char date_buf[32];
806 std::strftime(date_buf, sizeof(date_buf), "%Y-%m-%dT%H:%M:%S", utc);
807 char full_buf[48];
808 std::snprintf(full_buf, sizeof(full_buf), "%s.%09lldZ",
809 date_buf, static_cast<long long>(ns_part));
810 return full_buf;
811 }
812
813 static std::string inference_type_str(InferenceType t) {
814 switch (t) {
815 case InferenceType::CLASSIFICATION: return "CLASSIFICATION";
816 case InferenceType::REGRESSION: return "REGRESSION";
817 case InferenceType::EMBEDDING: return "EMBEDDING";
818 case InferenceType::GENERATION: return "GENERATION";
819 case InferenceType::RANKING: return "RANKING";
820 case InferenceType::ANOMALY: return "ANOMALY";
821 case InferenceType::RECOMMENDATION: return "RECOMMENDATION";
822 case InferenceType::CUSTOM: return "CUSTOM";
823 }
824 return "UNKNOWN";
825 }
826
827 static constexpr size_t MAX_FIELD_LENGTH = 4096;
828
830 static std::string random_hex_suffix_() {
831 uint8_t buf[4];
832#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__)
833 arc4random_buf(buf, sizeof(buf));
834#elif defined(_WIN32)
835 for (auto& b : buf) {
836 unsigned int val;
837 rand_s(&val);
838 b = static_cast<uint8_t>(val);
839 }
840#else
841 size_t written = 0;
842 while (written < sizeof(buf)) {
843 ssize_t ret = getrandom(buf + written, sizeof(buf) - written, 0);
844 if (ret > 0) written += static_cast<size_t>(ret);
845 else break;
846 }
847#endif
848 static constexpr char hex[] = "0123456789abcdef";
849 std::string result(8, '\0');
850 for (size_t i = 0; i < 4; ++i) {
851 result[2 * i] = hex[buf[i] >> 4];
852 result[2 * i + 1] = hex[buf[i] & 0x0F];
853 }
854 return result;
855 }
856
857 static std::string truncate_field(const std::string& s) {
858 if (s.size() <= MAX_FIELD_LENGTH) return s;
859 return s.substr(0, MAX_FIELD_LENGTH) + "...[TRUNCATED]";
860 }
861
862 static std::string j(const std::string& s) {
863 const std::string safe = truncate_field(s);
864 std::string out;
865 out.reserve(safe.size());
866 for (unsigned char c : safe) {
867 if (c == '"') out += "\\\"";
868 else if (c == '\\') out += "\\\\";
869 else if (c == '/') out += "\\/";
870 else if (c == '\n') out += "\\n";
871 else if (c == '\r') out += "\\r";
872 else if (c == '\t') out += "\\t";
873 else if (c < 0x20) { char buf[8];
874 std::snprintf(buf,sizeof(buf),"\\u%04x",c);
875 out += buf; }
876 else out += static_cast<char>(c);
877 }
878 return out;
879 }
880
881 static std::string dbl(double v) {
882 if (std::isnan(v) || std::isinf(v)) return "null";
883 char buf[32];
884 std::snprintf(buf, sizeof(buf), "%.6g", v);
885 return buf;
886 }
887
888 static std::string feats_preview(const std::vector<float>& f, size_t max_n) {
889 std::string o = "[";
890 const size_t n = (std::min)(f.size(), max_n);
891 for (size_t i = 0; i < n; ++i) {
892 char buf[16]; std::snprintf(buf, sizeof(buf), "%.4g", f[i]);
893 o += buf;
894 if (i + 1 < n) o += ",";
895 }
896 if (f.size() > max_n) o += ",...";
897 o += "]";
898 return o;
899 }
900};
901
902// ---------------------------------------------------------------------------
903// Art.15 Accuracy/Robustness Metrics (Gap R-3b)
904// ---------------------------------------------------------------------------
905
914 // --- Accuracy / Confidence ---
915 int64_t total_inferences = 0;
917 float low_confidence_rate = 0.0f;
918 float mean_confidence = 0.0f;
919 float median_confidence = 0.0f;
920 float std_dev_confidence = 0.0f;
921 float min_confidence = 1.0f;
922 float max_confidence = 0.0f;
923
924 // --- Latency / Robustness ---
925 int64_t mean_latency_ns = 0;
926 int64_t p50_latency_ns = 0;
927 int64_t p95_latency_ns = 0;
928 int64_t p99_latency_ns = 0;
929 int64_t max_latency_ns = 0;
930
931 // --- Drift Detection ---
933 float psi_score = 0.0f;
937
938 // --- Coverage ---
939 int64_t period_start_ns = 0;
940 int64_t period_end_ns = 0;
942
944 [[nodiscard]] std::string to_json(bool pretty = true) const {
945 const char* nl = pretty ? "\n" : "";
946 const char* sp = pretty ? " " : "";
947 const std::string ind = pretty ? " " : "";
948
949 std::string o = "{" + std::string(nl);
950 char buf[64];
951
952 o += ind + "\"total_inferences\":" + sp + std::to_string(total_inferences) + "," + nl;
953 o += ind + "\"low_confidence_count\":" + sp + std::to_string(low_confidence_count) + "," + nl;
954 std::snprintf(buf, sizeof(buf), "%.6f", low_confidence_rate);
955 o += ind + "\"low_confidence_rate\":" + sp + buf + "," + std::string(nl);
956 std::snprintf(buf, sizeof(buf), "%.6f", mean_confidence);
957 o += ind + "\"mean_confidence\":" + sp + buf + "," + std::string(nl);
958 std::snprintf(buf, sizeof(buf), "%.6f", median_confidence);
959 o += ind + "\"median_confidence\":" + sp + buf + "," + std::string(nl);
960 std::snprintf(buf, sizeof(buf), "%.6f", std_dev_confidence);
961 o += ind + "\"std_dev_confidence\":" + sp + buf + "," + std::string(nl);
962 std::snprintf(buf, sizeof(buf), "%.6f", min_confidence);
963 o += ind + "\"min_confidence\":" + sp + buf + "," + std::string(nl);
964 std::snprintf(buf, sizeof(buf), "%.6f", max_confidence);
965 o += ind + "\"max_confidence\":" + sp + buf + "," + std::string(nl);
966 o += ind + "\"mean_latency_ns\":" + sp + std::to_string(mean_latency_ns) + "," + nl;
967 o += ind + "\"p50_latency_ns\":" + sp + std::to_string(p50_latency_ns) + "," + nl;
968 o += ind + "\"p95_latency_ns\":" + sp + std::to_string(p95_latency_ns) + "," + nl;
969 o += ind + "\"p99_latency_ns\":" + sp + std::to_string(p99_latency_ns) + "," + nl;
970 o += ind + "\"max_latency_ns\":" + sp + std::to_string(max_latency_ns) + "," + nl;
971 o += ind + "\"distinct_model_versions\":" + sp + std::to_string(distinct_model_versions) + "," + nl;
972 std::snprintf(buf, sizeof(buf), "%.6f", psi_score);
973 o += ind + "\"psi_score\":" + sp + buf + "," + std::string(nl);
974 o += ind + "\"period_start_ns\":" + sp + std::to_string(period_start_ns) + "," + nl;
975 o += ind + "\"period_end_ns\":" + sp + std::to_string(period_end_ns) + "," + nl;
976 o += ind + "\"period_duration_ns\":" + sp + std::to_string(period_duration_ns) + nl;
977 o += "}";
978 return o;
979 }
980};
981
988public:
994 [[nodiscard]] static Art15Metrics compute(
995 const std::vector<InferenceRecord>& records,
996 float low_confidence_threshold = 0.5f) {
997 Art15Metrics m;
998
999 if (records.empty()) return m;
1000
1001 m.total_inferences = static_cast<int64_t>(records.size());
1002
1003 // Collect confidence scores and latencies
1004 std::vector<float> confidences;
1005 std::vector<int64_t> latencies;
1006 confidences.reserve(records.size());
1007 latencies.reserve(records.size());
1008
1009 std::vector<std::string> model_versions;
1010 double sum_conf = 0.0;
1011 int64_t sum_lat = 0;
1012 m.min_confidence = 1.0f;
1013 m.max_confidence = 0.0f;
1014
1015 for (const auto& rec : records) {
1016 confidences.push_back(rec.output_score);
1017 latencies.push_back(rec.latency_ns);
1018
1019 sum_conf += rec.output_score;
1020 if (rec.output_score < m.min_confidence) m.min_confidence = rec.output_score;
1021 if (rec.output_score > m.max_confidence) m.max_confidence = rec.output_score;
1022 if (rec.output_score < low_confidence_threshold) ++m.low_confidence_count;
1023
1024 sum_lat += rec.latency_ns;
1025
1026 // Track distinct model versions
1027 if (std::find(model_versions.begin(), model_versions.end(), rec.model_version)
1028 == model_versions.end()) {
1029 model_versions.push_back(rec.model_version);
1030 }
1031
1032 // Period bounds
1033 if (m.period_start_ns == 0 || rec.timestamp_ns < m.period_start_ns)
1034 m.period_start_ns = rec.timestamp_ns;
1035 if (rec.timestamp_ns > m.period_end_ns)
1036 m.period_end_ns = rec.timestamp_ns;
1037 }
1038
1039 auto n = static_cast<double>(records.size());
1040
1041 // Confidence statistics
1042 m.mean_confidence = static_cast<float>(sum_conf / n);
1043 m.low_confidence_rate = static_cast<float>(m.low_confidence_count) / static_cast<float>(n);
1044
1045 // Standard deviation
1046 double var_sum = 0.0;
1047 for (float c : confidences) {
1048 double diff = c - m.mean_confidence;
1049 var_sum += diff * diff;
1050 }
1051 m.std_dev_confidence = static_cast<float>(std::sqrt(var_sum / n));
1052
1053 // Median confidence
1054 std::sort(confidences.begin(), confidences.end());
1055 m.median_confidence = percentile(confidences, 0.5f);
1056
1057 // Latency statistics
1058 m.mean_latency_ns = sum_lat / static_cast<int64_t>(records.size());
1059 std::sort(latencies.begin(), latencies.end());
1060 m.p50_latency_ns = percentile_i64(latencies, 0.50f);
1061 m.p95_latency_ns = percentile_i64(latencies, 0.95f);
1062 m.p99_latency_ns = percentile_i64(latencies, 0.99f);
1063 m.max_latency_ns = latencies.back();
1064
1065 // Drift
1066 m.distinct_model_versions = static_cast<int64_t>(model_versions.size());
1068
1069 // PSI: split records into two halves (time-ordered) and compare
1070 // confidence distributions
1071 if (records.size() >= 20) {
1072 size_t half = records.size() / 2;
1073 std::vector<float> first_half, second_half;
1074 first_half.reserve(half);
1075 second_half.reserve(records.size() - half);
1076
1077 // Records are typically in chronological order
1078 for (size_t i = 0; i < half; ++i)
1079 first_half.push_back(records[i].output_score);
1080 for (size_t i = half; i < records.size(); ++i)
1081 second_half.push_back(records[i].output_score);
1082
1083 m.psi_score = compute_psi(first_half, second_half, 10);
1084 }
1085
1086 return m;
1087 }
1088
1089private:
1091 static float percentile(const std::vector<float>& sorted, float p) {
1092 if (sorted.empty()) return 0.0f;
1093 float idx = p * static_cast<float>(sorted.size() - 1);
1094 auto lo = static_cast<size_t>(idx);
1095 float frac = idx - static_cast<float>(lo);
1096 if (lo + 1 >= sorted.size()) return sorted.back();
1097 return sorted[lo] * (1.0f - frac) + sorted[lo + 1] * frac;
1098 }
1099
1100 static int64_t percentile_i64(const std::vector<int64_t>& sorted, float p) {
1101 if (sorted.empty()) return 0;
1102 auto idx = static_cast<size_t>(p * static_cast<float>(sorted.size() - 1));
1103 if (idx >= sorted.size()) idx = sorted.size() - 1;
1104 return sorted[idx];
1105 }
1106
1110 static float compute_psi(const std::vector<float>& reference,
1111 const std::vector<float>& current,
1112 int n_bins) {
1113 if (reference.empty() || current.empty() || n_bins <= 0) return 0.0f;
1114
1115 // Find global min/max
1116 float gmin = (std::min)(*std::min_element(reference.begin(), reference.end()),
1117 *std::min_element(current.begin(), current.end()));
1118 float gmax = (std::max)(*std::max_element(reference.begin(), reference.end()),
1119 *std::max_element(current.begin(), current.end()));
1120
1121 if (gmax <= gmin) return 0.0f;
1122
1123 float bin_width = (gmax - gmin) / static_cast<float>(n_bins);
1124 // Small epsilon to avoid division by zero
1125 constexpr float eps = 1e-6f;
1126
1127 std::vector<float> ref_pct(static_cast<size_t>(n_bins), 0.0f);
1128 std::vector<float> cur_pct(static_cast<size_t>(n_bins), 0.0f);
1129
1130 auto bin_idx = [&](float v) -> int {
1131 int b = static_cast<int>((v - gmin) / bin_width);
1132 if (b >= n_bins) b = n_bins - 1;
1133 if (b < 0) b = 0;
1134 return b;
1135 };
1136
1137 for (float v : reference) ref_pct[static_cast<size_t>(bin_idx(v))] += 1.0f;
1138 for (float v : current) cur_pct[static_cast<size_t>(bin_idx(v))] += 1.0f;
1139
1140 // Normalize to proportions
1141 float ref_total = static_cast<float>(reference.size());
1142 float cur_total = static_cast<float>(current.size());
1143 for (int i = 0; i < n_bins; ++i) {
1144 ref_pct[static_cast<size_t>(i)] = ref_pct[static_cast<size_t>(i)] / ref_total + eps;
1145 cur_pct[static_cast<size_t>(i)] = cur_pct[static_cast<size_t>(i)] / cur_total + eps;
1146 }
1147
1148 float psi = 0.0f;
1149 for (int i = 0; i < n_bins; ++i) {
1150 float p = cur_pct[static_cast<size_t>(i)];
1151 float q = ref_pct[static_cast<size_t>(i)];
1152 psi += (p - q) * std::log(p / q);
1153 }
1154
1155 return psi;
1156 }
1157};
1158
1159} // namespace signet::forge
Computes Art.15 accuracy, robustness, and drift metrics from inference records.
static Art15Metrics compute(const std::vector< InferenceRecord > &records, float low_confidence_threshold=0.5f)
Compute Art.15 metrics from a set of inference records.
static expected< DecisionLogReader > open(const std::string &path)
Open a decision log Parquet file and pre-load all column data.
EU AI Act compliance report generator (Regulation (EU) 2024/1689).
static expected< ComplianceReport > generate_article12(const std::vector< std::string > &inference_log_files, const ReportOptions &opts={})
Generate an Article 12 operational logging report from inference log files.
static expected< ComplianceReport > generate_article19(const std::vector< std::string > &decision_log_files, const std::vector< std::string > &inference_log_files, const ReportOptions &opts={})
Generate an Article 19 conformity assessment summary.
static expected< ComplianceReport > generate_article13(const std::vector< std::string > &inference_log_files, const ReportOptions &opts={})
Generate an Article 13 transparency disclosure from inference log files.
static expected< InferenceLogReader > open(const std::string &path)
Open an inference log Parquet file and pre-load all column data.
A lightweight result type that holds either a success value of type T or an Error.
Definition error.hpp:145
@ REJECTED
Order rejected by risk gate.
ComplianceStandard
Which regulatory standard a compliance report satisfies.
@ EU_AI_ACT_ART13
EU AI Act Article 13 — transparency disclosure.
@ EU_AI_ACT_ART19
EU AI Act Article 19 — conformity assessment summary.
@ EU_AI_ACT_ART12
EU AI Act Article 12 — operational logging.
@ JSON
Pretty-printed JSON object (default)
InferenceType
Classification of the ML inference operation.
@ REGRESSION
Continuous value prediction.
@ CUSTOM
Application-specific inference type.
@ CLASSIFICATION
Binary or multi-class classification.
@ RECOMMENDATION
Recommendation system inference.
@ EMBEDDING
Vector embedding computation.
@ GENERATION
LLM text generation.
@ ANOMALY
Anomaly/outlier detection.
@ RANKING
Ranking/scoring of candidates.
@ RISK_OVERRIDE
Risk gate override/rejection.
@ ORDER_NEW
Decision to submit a new order.
@ IO_ERROR
A file-system or stream I/O operation failed (open, read, write, rename).
@ INVALID_ARGUMENT
A caller-supplied argument is outside the valid range or violates a precondition.
Computed accuracy, robustness, and drift metrics per EU AI Act Art.15.
int64_t p99_latency_ns
99th percentile latency
float psi_score
Population Stability Index (0 = no drift)
int64_t p50_latency_ns
Median (p50) latency.
int64_t low_confidence_count
Inferences below low_confidence_threshold.
float median_confidence
Median output_score.
int64_t p95_latency_ns
95th percentile latency
float min_confidence
Minimum output_score observed.
int64_t mean_latency_ns
Mean inference latency.
float low_confidence_rate
low_confidence_count / total_inferences
float max_confidence
Maximum output_score observed.
int64_t distinct_model_versions
Number of distinct model versions seen.
float std_dev_confidence
Standard deviation of output_score.
int64_t max_latency_ns
Maximum latency.
std::string to_json(bool pretty=true) const
Serialize metrics to a JSON string.
float mean_confidence
Mean output_score across all inferences.
bool incomplete_data
H-20: True if one or more log file batches could not be read.
Lightweight error value carrying an ErrorCode and a human-readable message.
Definition error.hpp:101
Query and formatting parameters for compliance report generation.