The config defines the modes (modes.<name>): the kind of artifact the mode reviews - reply, series or commits, the three shipped partitions; a kind is a mode of its own name - the lenses it runs, in its kind's gate order, and the effort it runs at, else the kind's default (max for a reply or a series, xhigh for a commit range). `run --mode` names any of them, the dump records mode and kind, and everything that decided by the mode's name - the partition and hash, the prompts, the schemas, the disposition and reuse-key rules, what the CLI requires - decides by the kind. Every lens stays off until a mode lists it; lenses.<name>.run is retired with a warning that says so, and a lens of another kind, a name that is no lens, a lens listed twice and an unknown effort are config problems. The defaults run no lens; the template ships the three kinds as modes with every lens of their kind at the kind's effort, to trim (the owner's ruling of 2026-11-01), and init says so as it writes it - a kept config is the repository's own, and only one that lists no lens gets a step. The readers of the retired key follow: the CI smoke config, the shipped skill, the procedure (its description of the config as well as its mechanics), the manifest template, the run's messages and the comments that named the three modes. The run lock, the run numbers and the dump names are the kind's, so one full run per hash holds across the modes of a kind and their rounds count on together; the dump says which mode ran. The database broker starts for a lens the mode runs, as every other reader of the lenses off reads them. A named mode runs through the CLI in the kernel-modes test.
74 lines
4 KiB
C++
74 lines
4 KiB
C++
// 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; `persona` sets
|
|
// personaPass when given. 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<const std::string> drops = {}, const Json& read = nullptr, const Json& persona = nullptr);
|
|
// The whole state written as it is, through `<path>.tmp`.
|
|
void SaveWhole(const std::filesystem::path& path, const Json& state);
|
|
// Merge-on-write: the file re-read under `<path>.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<const std::string> drops = {}, const Json& read = nullptr, const Json& persona = 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-<kind>-<hash8>.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<Json> 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<const Json> records);
|
|
// The stdout summary of a run's dump.
|
|
std::string Summary(const Json& dump);
|
|
}
|