Crafter.Review/interfaces/Crafter.Review-Plan.cppm
Jorijn van der Graaf d5e54ffeb3
feat(config): modes - a mode is an artifact kind, the lenses it runs and its effort
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.
2026-11-02 00:29:00 +01:00

367 lines
23 KiB
C++

// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
export module Crafter.Review:Plan;
import std;
import :Json;
// The lens table, the unit kinds, pass planning, the mechanical n/a, the
// process budgets, the reuse keys and the cost estimate. A unit is the
// inventory's object ({id, lines, excerpt}) and a job the object PlanLens
// builds, both kept as JSON with their keys in a fixed order: the dump,
// progress.json and the prompts are made from them.
export namespace Crafter::Review::Plan {
// The artifact kinds. A mode (the config's modes.<name>) is a kind plus
// the lenses it runs and the effort it runs at; a kind is a mode of its
// own name.
constexpr std::array<std::string_view, 3> Kinds = {"reply", "series", "commits"};
bool IsKernelKind(std::string_view kind);
constexpr std::string_view FunctionalLens = "repo-functional.md";
// The persona lens of the kernel modes: one instance per reviewer
// (--reviewers), its whole pass gating its blocks, in the gate after
// every other.
constexpr std::string_view PersonaLens = "maintainer-persona.md";
constexpr std::string_view PersonaGate = "Gate 4 — Personas";
// The commit-truth lens (2026-10-31, the fp6 pipeline's repo-facts): the
// messages and the documentation of the range.
constexpr std::string_view FactsLens = "repo-facts.md";
// The reasoning effort when --effort is not given: the kernel modes
// stay at max; the push gates run at xhigh (2026-10-02).
constexpr std::string_view Effort = "xhigh";
constexpr std::string_view KernelEffort = "max";
// The efforts a mode may run at (modes.<name>.effort).
constexpr std::array<std::string_view, 5> Efforts = {"low", "medium", "high", "xhigh", "max"};
// Clean reads a code unit of a lens marked `reads` needs before its
// verdict is reused, one per round (--reads). 2026-10-06: a static lens
// is not deterministic — a second and third read of unchanged code still
// found defects; "3 is fine". A reply has no code: its paragraphs are
// read once.
constexpr std::int32_t Reads = 3;
constexpr std::int32_t ReplyReads = 1;
std::int32_t ReadsFor(std::string_view kind);
// The key's last slot held the run's effort from 2026-09-22 to
// 2026-10-02; it is frozen at the value every stored verdict was built
// with, so a clean verdict bought at one effort is reused at any other
// (Jorijn, 2026-10-02).
constexpr std::string_view ReuseKeyEffortSlot = "max";
// The model of each lens tier; every lens on Opus 5.5 (2026-09-25), and
// `--models fable=claude-fable-5-1` splits them. In table order: opus,
// fable.
struct ModelTier {
std::string tier;
std::string id;
};
std::vector<ModelTier>& Models();
// MODELS[tier]; Dyn::Error (KeyError) for an unknown tier.
const std::string& ModelFor(std::string_view tier);
constexpr std::array<std::string_view, 2> ApproachSlots = {"whole:order", "whole:absent"};
constexpr std::array<std::string_view, 1> BehaviourSlots = {"whole:behaviour"};
constexpr std::array<std::string_view, 1> CodeBlocks = {"code"};
constexpr std::array<std::string_view, 3> TruthBlocks = {"text", "doc", "slot"};
constexpr std::array<std::string_view, 2> TextSlotBlocks = {"text", "slot"};
constexpr std::array<std::string_view, 2> TextCodeBlocks = {"text", "code"};
constexpr std::array<std::string_view, 2> CodeSlotBlocks = {"code", "slot"};
// One row of LENSES. whole: "" (none), "first-repo" (the approach) or
// "functional" (the range exercised in the running application); every
// whole pass gates the lens's block passes and covers the slots named in
// `slots`. A lens with no whole pass and "slot" among its block kinds
// asks each slot named in `slots` in a slot pass of its own (the
// commit-truth lens: whole:order and whole:absent). block: the unit kinds
// the lens zooms into. reuse: per-unit reuse across hashes; wholeReuse:
// the clean whole pass carries while the code of the range is unchanged.
// key: what a code block is keyed on — "code" (its own hunk and the base;
// 2026-10-06) or "functional" (its hunk AND the code of the whole range:
// what the browser shows for a hunk depends on every other hunk); a text
// or doc block is keyed by its kind whatever the lens's key (TextReuseKey,
// DocReuseKey). reads: the lens's code units need Reads clean reads.
// empirical: the lens's evidence is observation, so it needs an
// environment with an application under test, and a whole pass that never
// used the instrument is not evidence. perCommitSlots false: the lens
// judges the per-commit slots in its whole pass. env: the default
// environment; a verdict bought in any other carries it in its key.
// kernel: a lens of the kernel modes - its whole pass and its slot
// passes cover every whole:* slot of the artifact, a hunk of a
// documentation file is code to it, and its keys and prompts are the
// kernel pipeline's.
struct Lens {
std::string_view file;
std::int32_t gate;
std::string_view whole;
std::span<const std::string_view> slots;
std::span<const std::string_view> block;
std::string_view model;
bool reuse;
bool wholeReuse;
bool reads;
std::string_view key;
bool empirical;
bool perCommitSlots;
std::string_view env;
bool kernel = false;
};
// In the order the dump and the prompts list them: the push gates'
// lenses, then the kernel modes'.
constexpr std::array<Lens, 15> Lenses = {{
{"repo-better-solution.md", 1, "first-repo", ApproachSlots, CodeBlocks, "fable", true, true, true, "code", false, true, "none"},
{FunctionalLens, 2, "functional", BehaviourSlots, CodeBlocks, "opus", true, true, false, "functional", true, false, "browser"},
{"repo-bug-prover.md", 3, "", {}, CodeBlocks, "opus", true, false, true, "code", false, true, "none"},
// The security lens (2026-10-22): does the range's code keep the
// protections the repository promises? The bug-prover's shape beside
// it in Gate 3: one gate stop for both proofs.
{"repo-security.md", 3, "", {}, CodeBlocks, "opus", true, false, true, "code", false, true, "none"},
// The commit-truth lens (2026-10-31): every claim in every commit
// message, and in the documentation the range adds or changes, proven
// or gone. The text and doc units and the two whole slots, in a gate
// of its own after the proofs, so the messages and documents are read
// once the code they describe has settled. One read: its units are
// prose, keyed on their text and the code they describe.
{FactsLens, 4, "", ApproachSlots, TruthBlocks, "opus", true, false, false, "text", false, true, "none"},
// The kernel modes (reply, series). whole: "first" judges the series
// as an approach, "all" the consistency of the whole, "persona" reads
// it as one reviewer. better-solution's code blocks are reused and
// need the clean reads like the other code lenses (2026-10-06.7); its
// whole pass has no key: it runs on every hash against what is on
// the list today. A persona's block passes are reusable, keyed with
// the persona's name (2026-10-06.6); its whole pass runs every round.
{"better-solution.md", 1, "first", {}, CodeBlocks, "fable", true, false, true, "code", false, true, "none", true},
{"code-quality.md", 2, "", {}, CodeBlocks, "opus", true, false, true, "code", false, true, "none", true},
{"minimalism.md", 2, "", {}, CodeSlotBlocks, "opus", true, false, true, "code", false, true, "none", true},
{"bug-prover.md", 2, "", {}, CodeBlocks, "opus", true, false, true, "code", false, true, "none", true},
{"facts.md", 3, "", {}, TextSlotBlocks, "opus", true, false, false, "code", false, true, "none", true},
{"consistency.md", 3, "all", {}, TextCodeBlocks, "opus", false, false, false, "code", false, true, "none", true},
{"process-docs.md", 3, "", {}, TextSlotBlocks, "opus", true, false, false, "code", false, true, "none", true},
{"etiquette.md", 3, "", {}, TextSlotBlocks, "opus", true, false, false, "code", false, true, "none", true},
{"conciseness.md", 3, "", {}, TextSlotBlocks, "fable", true, false, false, "code", false, true, "none", true},
{PersonaLens, 4, "persona", {}, TextCodeBlocks, "fable", true, false, false, "code", false, true, "none", true},
}};
bool IsLens(std::string_view file);
// LENSES[file]; Dyn::Error (KeyError) for a file that is no lens.
const Lens& LensOf(std::string_view file);
struct Gate {
std::string_view title;
std::vector<std::string_view> lenses;
};
// The gates of a mode: reply runs Gate 3 alone (facts, etiquette,
// consistency, conciseness); series Gate 1 (better-solution), Gate 2
// (code-quality, minimalism, bug-prover), Gate 3 (facts, consistency,
// process-docs, etiquette, conciseness); the persona gate follows them
// when reviewers are given. commits: the push gates.
const std::vector<Gate>& Gates(std::string_view kind = "commits");
// What a lens's processes get beyond the read-only tools, by the
// consumer's lenses.<name>.environment: mcp, the server rendered into
// --mcp-config ("" for none); tools, allowlist entries; app, the lens
// drives the application under test; db, the lens runs SQL in the
// database under test; memMb, what a pass is admitted at before one is
// measured. A new entry must first bring seven things (the environment in
// the reuse keys, its prompt texts, its own fail-closed check, validation,
// DENY, the opencode refusal, a firing proof); the tests hold the table at
// these four until then.
//
// memMb: the highest peak PSS of a pass's own process tree measured
// 2026-10-22 on the first consumer — the claude process alone 158-208 MB;
// with the browser 385-840 MB (Chromium 554-568, claude 208, the Playwright
// server 112-118, the SQL helper 5-9).
constexpr std::int32_t MemNone = 210;
constexpr std::int32_t MemBrowser = 840;
constexpr std::array<std::string_view, 1> BrowserTools = {"Bash(curl:*)"};
struct Environment {
std::string_view name;
std::string_view mcp;
std::span<const std::string_view> tools;
bool app;
bool db;
std::int32_t memMb;
};
constexpr std::array<Environment, 4> Environments = {{
{"none", "", {}, false, false, MemNone},
{"browser", "playwright", BrowserTools, true, false, MemBrowser},
{"supabase", "", {}, false, true, MemNone},
{"browser+supabase", "playwright", BrowserTools, true, true, MemBrowser},
}};
// nullptr for a name that is no environment.
const Environment* FindEnvironment(std::string_view name);
// The environment in a reuse key: evidence bought in one environment is
// never reused in another. The lens's default adds nothing, so every
// verdict stored before 2026-10-22 keeps its key. Empty `environment`:
// the default.
std::vector<std::string> EnvDeps(std::string_view lensFile, std::string_view environment = {});
// (max_turns, timeout_min) of a pass. Caps are safety nets, never retried
// (2026-09-17: a max-turns retry re-buys the whole context). A functional
// pass spends turns on the browser and gets more.
struct Budget {
std::int32_t maxTurns;
std::int32_t timeoutMin;
};
Budget BudgetFor(std::string_view kind, std::string_view unitKind, std::string_view lensFile);
std::string_view DefaultEffort();
std::string_view DefaultEffort(std::string_view kind);
// A text unit that is a bare section header of the camera review
// (`camss-milos`, `ov13b10-of`, `milos-dts-camss`, `across the three
// series`): nothing to judge, a mechanical n/a for every text lens.
bool IsBareHeader(const Json& unit, std::string_view text);
// "slot", "code" or "text".
std::string_view UnitKind(const Json& unit);
std::string_view UnitKindOf(std::string_view unitId);
// A hunk, a part of a hunk or the file header of a documentation file.
bool IsDocUnit(std::string_view unitId);
// `\.md$`, case-insensitively: a documentation file (plan.DOC_PATH and
// inventory.DOC_FILE, the same pattern).
bool IsDocPath(std::string_view path);
// The unit's kind for a lens: a documentation file's units are "doc".
std::string_view KindFor(std::string_view lensFile, const Json& unit);
// The file a hunk or file-header unit belongs to; nullopt for a slot or a
// text unit.
std::optional<std::string> UnitPath(std::string_view unitId);
// The units no lens of `lensFiles` (the lenses that run) plans a whole or
// block pass on. A mechanical n/a is not a reading.
std::vector<std::string> NotReviewed(const Json& units, std::span<const std::string> lines, std::span<const std::string> lensFiles,
std::span<const std::string> exclude = {});
// The first glob of functional.exclude the path matches (`**/` prefixes
// match at the root too).
std::optional<std::string> ExcludedBy(std::string_view path, std::span<const std::string> globs);
std::string UnitText(std::span<const std::string> lines, const Json& unit);
// sha256 of the text's UTF-8, in hex.
std::string Sha(std::string_view text);
// lens_file.replace('.md', ''); `persona:<name>` for a persona's
// instance.
std::string LensName(std::string_view lensFile, std::string_view persona = {});
// Every lens of the gates, and the persona lens in the kernel modes.
std::vector<std::string> KindLenses(std::string_view kind = "commits");
// The gates with their lens files. off: the lenses the config turns off;
// only keeps (test runs), without drops (an owner ruling); an empty span
// filters nothing, and a gate left empty disappears.
std::vector<Gate> GatesFor(std::span<const std::string> only = {}, std::span<const std::string> without = {},
std::span<const std::string> off = {});
std::vector<Gate> GatesFor(std::string_view kind, std::span<const std::string> only, std::span<const std::string> without,
std::span<const std::string> off);
// The whole job (null when the lens has none) and the block jobs.
struct LensPlan {
Json whole;
std::vector<Json> blocks;
};
// unitsFilter: --units (empty: every unit); exclude: the functional
// lens's path globs; persona: the reviewer a persona instance reads as;
// excludeUnits: the unit ids a persona's whole pass marked not-mine.
LensPlan PlanLens(std::string_view lensFile, const Json& units, std::span<const std::string> lines,
std::span<const std::string> unitsFilter = {}, std::span<const std::string> exclude = {}, std::string_view persona = {},
std::span<const std::string> excludeUnits = {});
// The model is part of the key (a verdict bought from one model is not
// evidence for another); the effort is not. ident replaces the unit id for
// a unit that is keyed on its content; a persona's block carries the
// persona's name. nullopt: the job is not reusable.
std::optional<std::string> ReuseKey(const Json& job, std::string_view lensFile, std::string_view unitText, std::vector<std::string> deps,
std::vector<std::string> inForceIds, std::string_view charterSha, std::int32_t promptVersion,
std::string_view ident = {});
// A code unit's text without what a rewrite of the range changes while
// the code stays: the blob ids of an `index` line, the line numbers of a
// hunk header, and the tail `git format-patch` appends (base-commit, the
// `-- ` signature, the git version), which the inventory hands to a file's
// last hunk.
std::string CodeNorm(std::string_view text);
// One format-patch file, cut where the inventory cuts it: whether the
// commit has a body, its code (every file section from the first
// `diff --git` on, normalised, without documentation files), the base,
// the diff headers of the documentation files, the message.
struct PatchParts {
bool hasBody = false;
std::string code;
std::string base;
std::vector<std::string> docs;
std::string message;
};
// docFiles false (a kernel series): nothing is documentation - a .md or
// .rst hunk is part of the code of its patch, as the kernel lenses read
// it.
PatchParts PartsOf(std::string_view patchText, bool docFiles = true);
// What the code keys of a range are built from, by commit number K.
struct CommitCode {
std::map<std::int64_t, bool> body;
std::map<std::int64_t, std::string> code;
std::map<std::int64_t, std::vector<std::string>> docs;
std::map<std::int64_t, std::string> message;
std::string range;
std::map<std::int64_t, std::string> upto;
std::string base;
bool kernel = false;
};
// kernel: the series mode's code - nothing is documentation, and the
// message of each patch is kept for its `absent` slot (a kernel reviewer
// looks there for a trailer or a test statement).
CommitCode CommitCodeOf(const std::map<std::int64_t, std::string>& patches, bool kernel = false);
// (ident, deps) of a code unit: a hunk or a file header is keyed on its
// file and its own normalised text, the two per-commit slots on that
// commit's position and code, the code of the whole range, its
// documentation files, and for `absent` whether it has a body (a kernel
// series: its message). Every code unit depends on the base.
std::pair<std::string, std::vector<std::string>> CodeKeyParts(std::string_view unitId, const CommitCode& cc);
std::optional<std::string> CodeReuseKey(const Json& job, std::string_view lensFile, std::string_view unitText, const CommitCode& cc,
std::span<const std::string> inForceIds, std::string_view charterSha, std::int32_t promptVersion,
std::string_view environment = {});
std::optional<std::string> FunctionalReuseKey(const Json& job, std::string_view lensFile, std::string_view unitText, const CommitCode& cc,
std::span<const std::string> inForceIds, std::string_view charterSha, std::int32_t promptVersion,
std::string_view environment = {});
// (ident, deps) of a doc unit - a hunk of a documentation file, or one
// heading-delimited part of it: keyed on its file and its own normalised
// text, like a code hunk, and on the base and the code of the whole
// range - a sentence must hold for the code as its commit leaves it AND
// as the range leaves it, so any code change in the range asks it again
// (2026-10-06).
std::pair<std::string, std::vector<std::string>> DocKeyParts(std::string_view unitId, const CommitCode& cc);
// What a message unit (pK:subject, pK:msg) depends on beside its own text:
// the base and the code of the range up to and including its commit - a
// message describes its commit's change and the state it changes, so a
// fix to that code asks the message again. Empty for every other unit (a
// diffstat states only its own numbers).
std::vector<std::string> MessageDeps(std::string_view unitId, const CommitCode& cc);
std::optional<std::string> DocReuseKey(const Json& job, std::string_view lensFile, std::string_view unitText, const CommitCode& cc,
std::span<const std::string> inForceIds, std::string_view charterSha, std::int32_t promptVersion,
std::string_view environment = {});
std::optional<std::string> TextReuseKey(const Json& job, std::string_view lensFile, std::string_view unitText, const CommitCode& cc,
std::span<const std::string> inForceIds, std::string_view charterSha, std::int32_t promptVersion,
std::string_view environment = {});
// By the job's unit kind (doc, text), else by the lens's `key` kind.
std::optional<std::string> BlockReuseKey(const Json& job, std::string_view lensFile, std::string_view unitText, const CommitCode& cc,
std::span<const std::string> inForceIds, std::string_view charterSha, std::int32_t promptVersion,
std::string_view environment = {});
// A whole pass that judges the code of the range: the code of every
// commit in order, the base, which commits have a body and which
// documentation files each touches. No message text and no commit id.
std::optional<std::string> WholeCodeKey(const Json& job, std::string_view lensFile, const CommitCode& cc, std::span<const std::string> inForceIds,
std::string_view charterSha, std::int32_t promptVersion, std::string_view environment = {});
// The dependencies of a text unit's verdict in the kernel modes: the
// URLs of every [n] it cites, the anchor text when it carries a dated or
// versioned statement (otherwise every anchor-date bump would invalidate
// every unit), and every R-unit it names verbatim. refs: the parsed
// reference table ({"n": url} rows).
std::vector<std::string> TextDeps(std::string_view unitText, const Json& units, std::span<const std::string> lines,
std::span<const std::pair<std::string, std::string>> refs, std::string_view anchorText);
// How many clean reads a stored result stands for: one per round that
// read its key clean.
std::int64_t ReadsOf(const Json& entry);
// Was this key already read clean in that round on that hash?
bool ReadIn(const Json& entry, std::string_view hash8, std::int64_t round);
// A stored result under the id its unit has in THIS artifact.
Json Rebind(const Json& entry, std::string_view unitId);
// Weighted cost-units (input 1, cache write 1.25, cache read 0.1, output
// 5) and list USD at opus rates (5 / 6.25 / 0.5 / 25 per M), calibrated on
// the 2026-09-17 smoke. lensLayerBytes: {lens name: bytes}.
Json Estimate(std::span<const Json> jobs, std::int64_t prefixBytes, const std::map<std::string, std::int64_t>& lensLayerBytes,
std::int64_t tailBytes = 1800, double tpb = 3.7, std::int64_t sysTokens = 32000, std::int64_t outPerTurn = 1200);
}