The modules that turn a frozen range into passes and passes into a verdict: :Schema, :Tools (the DENY, git and base lists, a leaf, so the config and the pool both read them), :Plan, :Artifact, :Inventory, :Dispositions, :State, :Normalize, :Stream, :Prompt, :Config, and the URL half of :PgWire. Units, jobs, results, the config and the claude stream stay JSON from end to end, so every key keeps its place; :Text gives JSON values truthiness, get, ==, membership and Str(), and throws Dyn::Error under a name for each kind of unexpected data. Exactness where the bytes are seen: - the stream classifiers are hand-written scanners with case-insensitive folds (the Kelvin sign is a k), Unicode \b and \s, and `.` stopping at \n; the SQL call detection takes `<exe> sql` with any path before it; - config problems carry the JSON scanner's and the UTF-8 decoder's own texts, positions included; - a float total is Neumaier's compensated sum, and the build passes -ffp-contract=off: -march=native would fuse a*b+c; - `crafter-review inventory` and `config` print what the goldens record, byte for byte (the fixture and 14 configs, broken ones included); a message names this binary as the command to run. tests/Goldens: the recorded answers over the fixtures next to them (the commits fixture, a rich versioned series with CRLF, a folded subject, Markdown headings in and out of fences, the code-key rewrites); the suite compares 2304 results as DumpFile writes them, so a key out of order fails too. All agree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
260 lines
14 KiB
C++
260 lines
14 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 {
|
|
constexpr std::string_view FunctionalLens = "repo-functional.md";
|
|
// The reasoning effort when --effort is not given (2026-10-02: the push
|
|
// gates run at xhigh).
|
|
constexpr std::string_view Effort = "xhigh";
|
|
// 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".
|
|
constexpr std::int32_t Reads = 3;
|
|
// 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"};
|
|
|
|
// 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`. 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 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). 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.
|
|
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;
|
|
};
|
|
// In the order the dump and the prompts list them.
|
|
constexpr std::array<Lens, 4> 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"},
|
|
}};
|
|
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;
|
|
};
|
|
const std::vector<Gate>& Gates();
|
|
|
|
// 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();
|
|
// "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', '')
|
|
std::string LensName(std::string_view lensFile);
|
|
// Every lens of the gates.
|
|
std::vector<std::string> ModeLenses();
|
|
// 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 = {});
|
|
|
|
// 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.
|
|
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 = {});
|
|
|
|
// 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. 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;
|
|
};
|
|
PatchParts PartsOf(std::string_view patchText);
|
|
|
|
// 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;
|
|
};
|
|
CommitCode CommitCodeOf(const std::map<std::int64_t, std::string>& patches);
|
|
|
|
// (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. 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 = {});
|
|
// 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 = {});
|
|
|
|
// 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);
|
|
}
|