133 lines
4.2 KiB
C++
133 lines
4.2 KiB
C++
// 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 <corpus_dir> [target_rate]
|
|
//
|
|
// Recursively globs `<corpus_dir>/**/*.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 <cstdio>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <random>
|
|
#include <string>
|
|
#include <system_error>
|
|
#include <vector>
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
namespace {
|
|
|
|
std::vector<fs::path> collect_pdfs(const fs::path& root, std::size_t cap)
|
|
{
|
|
std::vector<fs::path> 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 <corpus_dir> [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<std::string> 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<double>(byte_identical) /
|
|
static_cast<double>(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);
|
|
} |