92 const std::vector<std::string>& inference_log_files,
95 auto license = commercial::require_feature(
"EUAIActReporter::article12");
96 if (!license)
return license.error();
100 "Article 12 reports currently support JSON format only"};
102 if (inference_log_files.empty())
104 "EUAIActReporter: no inference log files supplied"};
106 std::vector<InferenceRecord> records;
107 bool chain_ok =
true;
108 std::string chain_id;
111 for (
const auto& path : inference_log_files) {
114 "EUAIActReporter: cannot open log file '" + path +
115 "': " + rdr_result.error().message};
116 auto& rdr = *rdr_result;
118 if (opts.verify_chain) {
119 auto vr = rdr.verify_chain();
120 if (!vr.valid) chain_ok =
false;
122 if (chain_id.empty()) {
123 auto meta = rdr.audit_metadata();
124 if (meta) chain_id = meta->chain_id;
126 auto all = rdr.read_all();
128 if (opts.strict_source_reads) {
130 "EUAIActReporter Art.12: failed to read '" + path +
131 "': " + all.error().message};
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));
142 std::sort(records.begin(), records.end(),
143 [](
const InferenceRecord& a,
const InferenceRecord& b){
144 return a.timestamp_ns < b.timestamp_ns; });
146 auto usage = commercial::record_usage_rows(
147 "EUAIActReporter::article12",
static_cast<uint64_t
>(records.size()));
148 if (!usage)
return usage.error();
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");
159 report.content = format_article12_json(records, opts, report);
174 const std::vector<std::string>& inference_log_files,
177 auto license = commercial::require_feature(
"EUAIActReporter::article13");
178 if (!license)
return license.error();
180 if (inference_log_files.empty())
182 "EUAIActReporter: no inference log files supplied"};
184 std::vector<InferenceRecord> records;
185 bool chain_ok =
true;
186 std::string chain_id;
189 for (
const auto& path : inference_log_files) {
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;
199 if (chain_id.empty()) {
200 auto meta = rdr.audit_metadata();
201 if (meta) chain_id = meta->chain_id;
203 auto all = rdr.read_all();
205 if (opts.strict_source_reads) {
207 "EUAIActReporter Art.13: failed to read '" + path +
208 "': " + all.error().message};
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));
219 auto usage = commercial::record_usage_rows(
220 "EUAIActReporter::article13",
static_cast<uint64_t
>(records.size()));
221 if (!usage)
return usage.error();
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");
232 report.content = format_article13_json(records, opts, report);
249 const std::vector<std::string>& decision_log_files,
250 const std::vector<std::string>& inference_log_files,
253 auto license = commercial::require_feature(
"EUAIActReporter::article19");
254 if (!license)
return license.error();
256 if (decision_log_files.empty() && inference_log_files.empty())
258 "EUAIActReporter: no log files supplied"};
261 std::vector<DecisionRecord> dec_records;
262 bool dec_chain_ok =
true;
263 std::string dec_chain_id;
264 int dec_read_errors = 0;
266 for (
const auto& path : decision_log_files) {
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;
276 if (dec_chain_id.empty()) {
277 auto meta = rdr.audit_metadata();
278 if (meta) dec_chain_id = meta->chain_id;
280 auto all = rdr.read_all();
282 if (opts.strict_source_reads) {
284 "EUAIActReporter Art.19: failed to read decision log '" + path +
285 "': " + all.error().message};
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));
297 std::vector<InferenceRecord> inf_records;
298 bool inf_chain_ok =
true;
299 std::string inf_chain_id;
300 int inf_read_errors = 0;
302 for (
const auto& path : inference_log_files) {
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;
312 if (inf_chain_id.empty()) {
313 auto meta = rdr.audit_metadata();
314 if (meta) inf_chain_id = meta->chain_id;
316 auto all = rdr.read_all();
318 if (opts.strict_source_reads) {
320 "EUAIActReporter Art.19: failed to read inference log '" + path +
321 "': " + all.error().message};
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));
337 if (opts.verify_chain &&
338 !dec_chain_id.empty() && !inf_chain_id.empty() &&
339 dec_chain_id != inf_chain_id) {
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."};
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());
350 auto usage = commercial::record_usage_rows(
351 "EUAIActReporter::article19",
static_cast<uint64_t
>(total));
352 if (!usage)
return usage.error();
354 ComplianceReport report = make_report_skeleton(
356 dec_chain_id.empty() ? inf_chain_id : dec_chain_id);
357 if (dec_read_errors > 0) {
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");
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");
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);
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;
388 int64_t total_input_tokens = 0;
389 int64_t total_output_tokens = 0;
390 int64_t total_batches = 0;
393 static PerfStats compute_perf(
const std::vector<InferenceRecord>& recs,
394 float low_conf_thr) {
396 s.total =
static_cast<int64_t
>(recs.size());
397 if (recs.empty())
return s;
399 std::vector<int64_t> latencies;
400 latencies.reserve(recs.size());
401 double sum_lat = 0.0, sum_conf = 0.0;
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;
413 s.avg_latency_ns = sum_lat /
static_cast<double>(recs.size());
414 s.avg_confidence = sum_conf /
static_cast<double>(recs.size());
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]);
421 s.p50_latency_ns = pct(0.50);
422 s.p95_latency_ns = pct(0.95);
423 s.p99_latency_ns = pct(0.99);
427 for (int64_t lat : latencies) {
428 double diff =
static_cast<double>(lat) - s.avg_latency_ns;
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;
443 static std::string format_article12_json(
444 const std::vector<InferenceRecord>& records,
445 const ReportOptions& opts,
446 const ComplianceReport& meta) {
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 ?
" " :
"";
453 PerfStats ps = compute_perf(records, opts.low_confidence_threshold);
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)
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;
485 double per_record_sigma3 = 0.0;
486 if (!records.empty()) {
488 for (
const auto& r : records) {
489 double diff =
static_cast<double>(r.latency_ns) - ps.avg_latency_ns;
492 var /=
static_cast<double>(records.size());
493 per_record_sigma3 = ps.avg_latency_ns + 3.0 * std::sqrt(var);
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;
521 o += ind2 + ind +
"\"anomaly\":" + sp
522 + (rec.latency_ns >
static_cast<int64_t
>(per_record_sigma3)
524 if (opts.include_features && !rec.input_embedding.empty()) {
526 o += ind2 + ind +
"\"input_embedding_preview\":" + sp
527 + feats_preview(rec.input_embedding, 8);
529 o += nl + ind2 +
"}";
530 if (i + 1 < records.size()) o +=
",";
538 static std::string format_article13_json(
539 const std::vector<InferenceRecord>& records,
540 const ReportOptions& opts,
541 const ComplianceReport& meta) {
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 ?
" " :
"";
548 PerfStats ps = compute_perf(records, opts.low_confidence_threshold);
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;
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;
574 o += ind +
"\"provider\":" + sp +
"{" + nl;
575 o += ind2 +
"\"name\":" + sp +
"\""
576 + j(opts.provider_name.empty() ?
"UNSPECIFIED" : opts.provider_name)
578 o += ind2 +
"\"contact\":" + sp +
"\""
579 + j(opts.provider_contact.empty() ?
"UNSPECIFIED" : opts.provider_contact)
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;
602 if (!opts.bias_risks.empty()) {
603 o += ind +
"\"bias_risks\":" + sp +
"\""
604 + j(opts.bias_risks) +
"\"," + nl;
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")
613 o += ind +
"}," + nl;
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"
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]) +
"\"";
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
642 ?
static_cast<double>(ps.low_conf_count) / ps.total
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
652 ?
static_cast<double>(ps.total_batches) / ps.total
656 std::string td_id, td_chars;
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;
666 if (!td_id.empty() || td_size > 0 || !td_chars.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()) {
675 o += ind2 +
"\"training_data_characteristics\":" + sp
676 +
"\"" + j(td_chars) +
"\"";
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;
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) {
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 ?
" " :
"";
705 PerfStats ps = compute_perf(inf_recs, opts.low_confidence_threshold);
708 int64_t orders_new = 0, orders_rejected = 0, risk_overrides = 0;
709 for (
const auto& r : dec_recs) {
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\"")
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;
761 static ComplianceReport make_report_skeleton(
763 const ReportOptions& opts,
764 int64_t total_records,
766 const std::string& chain_id) {
770 r.format = opts.format;
771 r.chain_verified = chain_ok;
772 r.chain_id = chain_id;
773 r.total_records = total_records;
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)())
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_())
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);
800 gmtime_s(&tm_buf, &t);
801 std::tm* utc = &tm_buf;
803 std::tm* utc = gmtime_r(&t, &tm_buf);
806 std::strftime(date_buf,
sizeof(date_buf),
"%Y-%m-%dT%H:%M:%S", utc);
808 std::snprintf(full_buf,
sizeof(full_buf),
"%s.%09lldZ",
809 date_buf,
static_cast<long long>(ns_part));
827 static constexpr size_t MAX_FIELD_LENGTH = 4096;
830 static std::string random_hex_suffix_() {
832#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__)
833 arc4random_buf(buf,
sizeof(buf));
835 for (
auto& b : buf) {
838 b =
static_cast<uint8_t
>(val);
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);
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];
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]";
862 static std::string j(
const std::string& s) {
863 const std::string safe = truncate_field(s);
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);
876 else out +=
static_cast<char>(c);
881 static std::string dbl(
double v) {
882 if (std::isnan(v) || std::isinf(v))
return "null";
884 std::snprintf(buf,
sizeof(buf),
"%.6g", v);
888 static std::string feats_preview(
const std::vector<float>& f,
size_t max_n) {
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]);
894 if (i + 1 < n) o +=
",";
896 if (f.size() > max_n) o +=
",...";