Crafter.Review/interfaces/Crafter.Review-Plan.cppm

260 lines
14 KiB
Text
Raw Normal View History

feat(pipeline): the pure pipeline, checked against its goldens 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>
2026-10-31 06:48:55 +01:00
// 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);
}