// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® export module Crafter.Review:State; import std; import :Json; // unit-state.json (per-unit reuse), the run lock, the usage totals and the // stdout summary. unit-state.json is merged on write, because drivers share // one gate-state dir, and one full run per hash holds the hash's lock: every // driver on the machine reads and writes these, so the files, the locks and // the layout do not change. export namespace Crafter::Review::State { // {version: 1, entries: {}, personaPass: null} Json EmptyState(); // The file as JSON, whatever its shape, or the empty state when it is // missing or does not parse. Json LoadState(const std::filesystem::path& path); // This run's stores and drops applied to the state on disk — pure. Drops // win over sets; an entry of `sets` that is not a whole pass gets its // clean-read count from the FILE's previous entry (or the one-read // synthesis of an entry stored before the count was kept) plus `read` // unless that (hash, round) is counted already. Garbage on disk is the // empty state. `sets` is an object; a null `read` counts nothing. Json MergeState(const Json& disk, const Json& sets, std::span drops = {}, const Json& read = nullptr); // The whole state written as it is, through `.tmp`. void SaveWhole(const std::filesystem::path& path, const Json& state); // Merge-on-write: the file re-read under `.lock`, this run's deltas // applied (MergeState), written atomically; returns the merged state. Two // drivers on one gate-state dir would otherwise overwrite each other's // stores, and a --no-reuse run would wipe the file from its empty object. Json SaveState(const std::filesystem::path& path, const Json& sets, std::span drops = {}, const Json& read = nullptr); // ---- one full run per hash ------------------------------------------------- // Another full run holds this hash's lock; what() names the holder. class RunLocked : public std::runtime_error { public: using std::runtime_error::runtime_error; }; std::filesystem::path LockPath(const std::filesystem::path& gateDir, std::string_view mode, std::string_view hash8); // The holder's JSON, {} when there is none or it does not parse. Json ReadLockInfo(const std::filesystem::path& path); std::string Describe(const Json& info); // An exclusive, non-blocking flock on gate-state/.run--.lock, // kept for the life of the process: the kernel releases it when the // holder dies, and no child inherits it. The file is never unlinked: an // unlink and re-create would hand two drivers one hash. class RunLock { public: // Throws RunLocked naming the holder. RunLock(const std::filesystem::path& gateDir, std::string_view mode, std::string_view hash8); ~RunLock(); RunLock(const RunLock&) = delete; RunLock& operator=(const RunLock&) = delete; // The holder's identity, once it knows its run number: one JSON // document, truncated and rewritten. void WriteInfo(const Json& info); private: std::int32_t fd = -1; }; // Who holds this hash's run lock ({} while the holder has not written // itself yet), nullopt when nobody does: a test or dry run takes no lock // but numbers its scratch dir past a live holder. std::optional LockHolder(const std::filesystem::path& gateDir, std::string_view mode, std::string_view hash8); std::int64_t NextRunNumber(const std::filesystem::path& gateDir, std::string_view mode, std::string_view hash8); // The usage of a set of pass records, summed in the order they come. Json UsageTotals(std::span records); // The stdout summary of a run's dump. std::string Summary(const Json& dump); }