// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® module Crafter.Review:Refs_impl; import :Refs; import :Json; import :Text; import :Platform; import :Process; import std; namespace fs = std::filesystem; namespace Crafter::Review::Refs { namespace { constexpr std::string_view LoreHosts[] = {"https://lore.kernel.org/", "http://lore.kernel.org/"}; bool IsListChar(char c) { return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-'; } // LORE_RE at `at`: https?://lore\.kernel\.org/(?:r|all|[a-z0-9-]+)/([^/\s>]+)/? // -> (the end of the match, the message id), or nullopt. std::optional> LoreAt(std::string_view text, std::size_t at) { std::size_t after = 0; for (const std::string_view host : LoreHosts) { if (text.substr(at).starts_with(host)) { after = at + host.size(); break; } } if (after == 0) return std::nullopt; std::size_t list = after; while (list < text.size() && IsListChar(text[list])) ++list; if (list == after || list >= text.size() || text[list] != '/') return std::nullopt; std::size_t id = list + 1; std::size_t end = id; while (end < text.size()) { const auto [c, used] = Dyn::CodePointAt(text, end); if (c == '/' || c == '>' || Dyn::IsSpace(c)) break; end += used; } if (end == id) return std::nullopt; const std::string msgid(text.substr(id, end - id)); if (end < text.size() && text[end] == '/') ++end; return std::pair(end, msgid); } Fetcher& TheFetcher() { static Fetcher fetcher; return fetcher; } Searcher& TheSearcher() { static Searcher searcher; return searcher; } std::string Tail(std::string_view text, std::size_t n) { return Dyn::Slice(text, -static_cast(n)); } } Table ParseRefs(std::string_view text) { Table refs; auto set = [&](const std::string& key, const std::string& url) { for (auto& [k, u] : refs) { if (k == key) { u = url; return; } } refs.emplace_back(key, url); }; // ^\[(\d+)\] (\S+), multiline. for (const std::string& line : Dyn::Split(text, "\n")) { if (!line.starts_with('[')) continue; const std::size_t digits = Dyn::DecimalRun(line, 1); if (digits == 0 || !std::string_view(line).substr(1 + digits).starts_with("] ")) continue; const std::size_t urlAt = 1 + digits + 2; const std::size_t urlLen = Dyn::NonSpaceRun(line, urlAt); if (urlLen == 0) continue; const std::optional n = Dyn::ParseInt(line.substr(1, digits)); if (!n) continue; set(std::to_string(*n), line.substr(urlAt, urlLen)); } if (!refs.empty()) return refs; std::vector seen; for (std::size_t at = 0; at < text.size();) { const std::optional> found = LoreAt(text, at); if (!found) { ++at; continue; } const std::string whole(text.substr(at, found->first - at)); if (!std::ranges::contains(seen, whole)) seen.push_back(whole); at = found->first; } for (std::size_t k = 0; k < seen.size(); ++k) refs.emplace_back(std::format("L{}", k + 1), seen[k]); return refs; } std::optional UrlOf(const Table& refs, std::int64_t n) { const std::string key = std::to_string(n); for (const auto& [k, url] : refs) { if (k == key) return url; } return std::nullopt; } std::optional MsgidOf(std::string_view url) { for (std::size_t at = 0; at < url.size(); ++at) { if (const std::optional> found = LoreAt(url, at)) return found->second; } return std::nullopt; } std::string SafeName(std::string_view msgid) { std::u32string out; for (const char32_t c : Dyn::Decode(msgid)) { const bool kept = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '@' || c == '-'; out.push_back(kept ? c : U'_'); } if (out.size() > 200) out.resize(200); return Dyn::Encode(out); } std::map Headers(std::string_view raw) { std::map out; const std::string head = Dyn::Split(raw, "\n\n", 1).front(); for (const std::string& line : Dyn::SplitLines(head)) { for (const std::string_view key : {"From", "Date", "Subject"}) { const std::string prefix = std::format("{}:", key); if (line.starts_with(prefix) && !out.contains(std::string(key))) out[std::string(key)] = Dyn::Strip(std::string_view(line).substr(prefix.size())); } } return out; } void SetFetcher(Fetcher fetcher) { TheFetcher() = std::move(fetcher); } std::pair Fetch(const fs::path& repo, const std::string& url, const fs::path& path) { if (TheFetcher()) return TheFetcher()(repo, url, path); std::string last; for (std::int32_t attempt = 0; attempt < 3; ++attempt) { Process::Captured p; try { p = Process::Run({"python3", "utilities/lore.py", "fetch", std::format("{}/raw", Dyn::RStrip(url, "/"))}, repo, std::chrono::milliseconds(180000)); } catch (const std::exception& e) { last = Dyn::Slice(std::format("{}: {}", "RuntimeError", e.what()), 0, 200); std::this_thread::sleep_for(std::chrono::seconds(2)); continue; } if (p.timedOut) { last = Dyn::Slice(std::format("TimeoutExpired: Command '{}' timed out after 180 seconds", url), 0, 200); std::this_thread::sleep_for(std::chrono::seconds(2)); continue; } if (p.returnCode == 0 && !Dyn::Strip(p.out).empty()) { Platform::WriteAtomic(path, p.out); return {"fetched", ""}; } last = Tail(Dyn::DecodeReplace(p.err.empty() ? p.out : p.err), 200); std::this_thread::sleep_for(std::chrono::seconds(2)); } return {"failed", last}; } Json Prefetch(const fs::path& repo, const Table& refs, const fs::path& refsDir, const fs::path& indexPath, const Log& log, std::int32_t workers) { fs::create_directories(refsDir); Json index = Json::object(); struct Todo { std::string key; std::string url; fs::path path; }; std::vector todo; for (const auto& [n, url] : refs) { const std::optional mid = MsgidOf(url); if (!mid) { index[n] = {{"url", url}, {"status", "not-prefetched (not a lore URL)"}}; continue; } const fs::path path = refsDir / std::format("{}.raw", SafeName(*mid)); index[n] = {{"url", url}, {"msgid", *mid}, {"path", Dyn::RelPath(path.string(), repo.string())}}; std::error_code error; if (fs::exists(path, error) && fs::file_size(path, error) > 0) { index[n]["status"] = "cached"; } else { todo.push_back({n, url, path}); } } std::vector> results(todo.size()); { std::atomic next = 0; std::vector pool; const std::size_t width = std::min(todo.size(), static_cast(std::max(1, workers))); for (std::size_t w = 0; w < width; ++w) { pool.emplace_back([&] { for (std::size_t i = next.fetch_add(1); i < todo.size(); i = next.fetch_add(1)) results[i] = Fetch(repo, todo[i].url, todo[i].path); }); } } for (std::size_t i = 0; i < todo.size(); ++i) { index[todo[i].key]["status"] = results[i].first; if (!results[i].second.empty()) index[todo[i].key]["error"] = results[i].second; } for (auto it = index.begin(); it != index.end(); ++it) { Json& e = it.value(); const Json& status = Dyn::Get(e, "status"); if (!Dyn::Truthy(Dyn::Get(e, "path")) || !(status == "cached" || status == "fetched")) continue; const std::optional bytes = Platform::ReadFile(repo / Dyn::Str(e.at("path"))); if (!bytes) { e["status"] = "failed"; e["error"] = std::format("[Errno 2] No such file or directory: {}", Dyn::Repr((repo / Dyn::Str(e.at("path"))).string())); continue; } const std::string raw = Dyn::DecodeReplace(*bytes); for (const auto& [k, v] : Headers(raw)) e[k] = v; e["bytes"] = Dyn::Length(raw); } Platform::WriteAtomic(indexPath, DumpFile(index)); std::int64_t ok = 0; std::int64_t fetched = 0; for (auto it = index.begin(); it != index.end(); ++it) { const Json& status = Dyn::Get(it.value(), "status"); if (status == "cached" || status == "fetched") ++ok; if (status == "fetched") ++fetched; } log(std::format("refs: {}/{} lore references available locally ({} fetched now)", ok, index.size(), fetched)); return index; } std::string Digest(const Json& index) { std::vector lines = { "REFERENCES prefetched (read the raw file instead of fetching; a FETCH FAILED line means the reference must be fetched by you or reported as unverifiable):"}; std::vector keys; for (auto it = index.begin(); it != index.end(); ++it) keys.push_back(it.key()); auto numeric = [](const std::string& k) { return !k.empty() && Dyn::DecimalRun(k, 0) == k.size(); }; std::ranges::sort(keys, [&](const std::string& a, const std::string& b) { const bool na = numeric(a); const bool nb = numeric(b); if (na != nb) return na; if (na) return *Dyn::ParseInt(a) < *Dyn::ParseInt(b); return a < b; }); const Json unknown = "?"; for (const std::string& n : keys) { const Json& e = index.at(n); const Json& status = Dyn::Get(e, "status"); if (status == "cached" || status == "fetched") { const Json& subject = Dyn::Has(e, "Subject") ? e.at("Subject") : unknown; lines.push_back(std::format("[{}] {} | From: {} | Date: {} | Subject: {} | {} ({} B)", n, Dyn::Str(Dyn::Get(e, "msgid")), Dyn::Str(Dyn::Has(e, "From") ? e.at("From") : unknown), Dyn::Str(Dyn::Has(e, "Date") ? e.at("Date") : unknown), Dyn::Slice(Dyn::Str(subject), 0, 100), Dyn::Str(e.at("path")), Dyn::Str(Dyn::Has(e, "bytes") ? e.at("bytes") : Json(0)))); } else if (status == "failed") { lines.push_back(std::format("[{}] {} | FETCH FAILED | {}", n, Dyn::Str(Dyn::Get(e, "msgid")), Dyn::Str(e.at("url")))); } else { lines.push_back(std::format("[{}] {} | {}", n, Dyn::Str(e.at("url")), Dyn::Str(status))); } } return Dyn::Join(lines, "\n"); } void SetSearcher(Searcher searcher) { TheSearcher() = std::move(searcher); } std::pair, std::string> PersonaCorpus(const fs::path& repo, std::string_view name, const fs::path& refsDir, std::string_view since, const Log& log) { // re.sub(r'[^a-z0-9]+', '-', name.lower()).strip('-') std::u32string slug; bool dash = false; for (const char32_t c : Dyn::Decode(Dyn::Lower(name))) { if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) { slug.push_back(c); dash = false; } else if (!dash) { slug.push_back(U'-'); dash = true; } } const std::string slugText = Dyn::Strip(Dyn::Encode(slug), "-"); const fs::path path = refsDir / std::format("persona-{}.txt", slugText); std::error_code error; if (fs::exists(path, error) && fs::file_size(path, error) > 0) return {path, Dyn::ReadText(path)}; const std::string q = std::format("f:\"{}\" AND d:{}..", name, since); std::string out; std::string err; if (TheSearcher()) { std::tie(out, err) = TheSearcher()(repo, q); } else { try { const Process::Captured p = Process::Run({"python3", "utilities/lore.py", "search", "-n", "100", q}, repo, std::chrono::milliseconds(180000)); out = Dyn::DecodeReplace(p.out); err = Dyn::DecodeReplace(p.err); } catch (const std::exception& e) { err = e.what(); } } const std::string text = Dyn::Strip(out); if (text.empty() || Dyn::Lower(text).starts_with("no results")) { log(std::format("persona corpus: {} -> NO RESULTS for {} (stderr: {}) - the whole pass calibrates by its own searches", name, Dyn::Repr(q), Dyn::Slice(Dyn::Strip(err), 0, 120))); return {std::nullopt, ""}; } fs::create_directories(refsDir); Platform::WriteAtomic(path, std::format("lore.py search {} ({})\n{}\n", Dyn::Repr(q), Platform::LocalTime("%Y-%m-%d"), text)); log(std::format("persona corpus: {} -> {} ({} chars)", name, Dyn::RelPath(path.string(), repo.string()), Dyn::Length(text))); return {path, Dyn::ReadText(path)}; } std::string SinceOf(std::string_view isoDate) { const std::vector parts = Dyn::Split(isoDate, "-"); if (parts.size() != 3) throw Dyn::Error("ValueError", std::format("Invalid isoformat string: {}", Dyn::Repr(isoDate))); const std::optional y = Dyn::ParseInt(parts[0]); const std::optional m = Dyn::ParseInt(parts[1]); const std::optional d = Dyn::ParseInt(parts[2]); if (!y || !m || !d) throw Dyn::Error("ValueError", std::format("Invalid isoformat string: {}", Dyn::Repr(isoDate))); const std::chrono::year_month_day date{std::chrono::year(static_cast(*y)), std::chrono::month(static_cast(*m)), std::chrono::day(static_cast(*d))}; if (!date.ok()) throw Dyn::Error("ValueError", std::format("Invalid isoformat string: {}", Dyn::Repr(isoDate))); const std::chrono::year_month_day since{std::chrono::sys_days(date) - std::chrono::days(365)}; return std::format("{:04}{:02}{:02}", static_cast(since.year()), static_cast(since.month()), static_cast(since.day())); } }