Every claim in every commit message, and in the documentation the range adds or changes, proven or gone. The lens's block kinds are text, doc and slot: a message unit is keyed on its text, the base and the code up to its commit (MessageDeps); a documentation unit on its file, its normalised text, the base and the code of the whole range (DocKeyParts); a diffstat on its text alone; whole:order and whole:absent are asked in slot passes of its own, never stored. A documentation file's diff header and a part the commit writes nothing in are a mechanical n/a. Budgets 60/40 for a text or doc block, 50/35 for a slot; one read. Gate 4, after the proofs, so the messages and documents are read once the code they describe has settled; the owner's gate is 5. The goldens that follow from the lens table are derived by tests/Goldens/derive.py, which asserts that the recorded table still reproduces the recorded entries before it writes. Proven, with the opt-in commit after this one: the suite (26 passed); a dry run of a scratch range (2 commits, a README with sections) by the previous binary and this one with the three static lenses on and the rest off, 25 prompts and 4 system files byte for byte; with repo-facts on as well, those files unchanged and 10 passes and 1 system file added. The lens's firing proof in this pipeline is owed (the owner's go).
294 lines
17 KiB
C++
294 lines
17 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 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 (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"};
|
|
constexpr std::array<std::string_view, 3> TruthBlocks = {"text", "doc", "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.
|
|
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, 5> 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"},
|
|
}};
|
|
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 = {});
|
|
// (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 = {});
|
|
|
|
// 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);
|
|
}
|