// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors // // Spike A main: walks a corpus directory, runs the QPDF verbatim round-trip on // every PDF, and emits a JSON report on stdout. Exit code 0 if the // byte-identical rate meets the §14 target (≥ 99%), 1 otherwise. // // Usage: // spike_a_verbatim_roundtrip [target_rate] // // Recursively globs `/**/*.pdf`. Output PDFs go into a sibling // temp directory next to the executable; nothing in the corpus is mutated. #include "Roundtrip.h" #include "common/SpikeRunner.h" #include #include #include #include #include #include #include namespace fs = std::filesystem; namespace { std::vector collect_pdfs(const fs::path& root, std::size_t cap) { std::vector out; std::error_code ec; if (!fs::exists(root, ec)) return out; for (auto& entry : fs::recursive_directory_iterator(root, fs::directory_options::skip_permission_denied, ec)) { if (ec) { ec.clear(); continue; } if (entry.is_regular_file(ec) && entry.path().extension() == ".pdf") { out.push_back(entry.path()); if (out.size() >= cap) break; } } return out; } fs::path make_temp_dir() { auto tmp = fs::temp_directory_path(); std::random_device rd; std::mt19937_64 gen(rd()); auto suffix = std::to_string(gen()); auto p = tmp / ("freepdfeditor-spikeA-" + suffix); fs::create_directories(p); return p; } } // namespace int main(int argc, char** argv) { if (argc < 2) { std::fprintf(stderr, "usage: %s [target_rate_default=0.99]\n", argv[0]); return 2; } fs::path corpus = argv[1]; double target = (argc >= 3) ? std::stod(argv[2]) : 0.99; auto pdfs = collect_pdfs(corpus, 100'000); if (pdfs.empty()) { freepdfeditor::spike::SpikeResult r{}; r.spike = "A"; r.name = "QPDF verbatim round-trip"; r.metric_name = "byte_identical_rate"; r.target = target; r.gate_met = false; r.notes = "no PDFs found in corpus directory"; return freepdfeditor::spike::emit_json_report(r); } auto outdir = make_temp_dir(); std::size_t byte_identical = 0; std::size_t structurally_same = 0; std::size_t failed = 0; std::vector failed_samples; for (std::size_t i = 0; i < pdfs.size(); ++i) { const auto& in = pdfs[i]; fs::path out = outdir / (std::to_string(i) + ".pdf"); fs::remove(out); auto fr = freepdfeditor::spike::a::roundtrip(in, out); switch (fr.outcome) { case freepdfeditor::spike::a::Outcome::ByteIdentical: ++byte_identical; break; case freepdfeditor::spike::a::Outcome::StructurallySame: ++structurally_same; break; case freepdfeditor::spike::a::Outcome::Failed: ++failed; if (failed_samples.size() < 16) { failed_samples.push_back(in.filename().string()); } break; } fs::remove(out); } std::error_code ec; fs::remove_all(outdir, ec); double rate = static_cast(byte_identical) / static_cast(pdfs.size()); // The §14 gate is byte-identity. Structural-same is interesting for the // report but does not pass the gate; we surface it in notes. bool gate = rate >= target; freepdfeditor::spike::SpikeResult r{}; r.spike = "A"; r.name = "QPDF verbatim round-trip"; r.total = pdfs.size(); r.passed = byte_identical; r.failed = structurally_same; r.errored = failed; r.metric_name = "byte_identical_rate"; r.metric_value = rate; r.target = target; r.gate_met = gate; r.failed_samples = failed_samples; r.notes = "structurally_same files (QPDF rewrote, semantics preserved) count " "as not-byte-identical for this gate; they are tracked in `failed`"; return freepdfeditor::spike::emit_json_report(r); }