Crafter.Review/implementations/Crafter.Review-Tools.cpp
Jorijn van der Graaf 741e3a656d
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

71 lines
3.3 KiB
C++

// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module Crafter.Review:Tools_impl;
import :Tools;
import std;
namespace Crafter::Review::Tools {
std::vector<std::string> McpToolNames() {
std::vector<std::string> out;
for (const std::string_view tool : McpTools) out.push_back(std::format("mcp__{}__{}", McpServer, tool));
return out;
}
std::vector<std::string> McpDeniedToolNames() {
std::vector<std::string> out;
for (const std::string_view tool : McpDenied) out.push_back(std::format("mcp__{}__{}", McpServer, tool));
return out;
}
const std::vector<std::string>& Deny() {
static const std::vector<std::string> deny = [] {
std::vector<std::string> out = {
"Edit", "Write", "MultiEdit", "NotebookEdit", "WebFetch", "WebSearch", "Task", "Agent", "TodoWrite",
"Bash(ssh:*)", "Bash(adb:*)", "Bash(rm:*)", "Bash(mv:*)", "Bash(cp:*)", "Bash(git push:*)", "Bash(git commit:*)",
"Bash(git checkout:*)", "Bash(git reset:*)", "Bash(git am:*)", "Bash(make:*)", "Bash(npm:*)", "Bash(npx:*)", "Bash(node:*)",
"Bash(supabase:*)", "Bash(docker:*)", "Bash(podman:*)", "Bash(psql:*)",
};
std::ranges::move(McpDeniedToolNames(), std::back_inserter(out));
return out;
}();
return deny;
}
const std::vector<std::string>& BaseAllow() {
static const std::vector<std::string> allow = [] {
std::vector<std::string> out = {
"Read", "Glob", "Grep", "Bash(cat:*)", "Bash(rg:*)", "Bash(grep:*)", "Bash(wc:*)", "Bash(head:*)", "Bash(tail:*)", "Bash(sed -n:*)",
"Bash(ls:*)", "Bash(find:*)", "Bash(diff:*)", "Bash(sha256sum:*)", "Bash(sort:*)", "Bash(uniq:*)", "Bash(cut:*)", "Bash(awk:*)",
"Bash(echo:*)", "Bash(printf:*)", "Bash(cd:*)", "Bash(python3 -:*)",
};
for (const std::string_view verb : GitReadOnly) out.push_back(std::format("Bash(git {}:*)", verb));
return out;
}();
return allow;
}
namespace {
// re.match(r'^Bash\((.+):\*\)$', tool).group(1): the prefix of a
// Bash(<prefix>:*) entry. `.` stops at a newline and `$` also matches
// before a final one, so a prefix never holds a newline.
std::optional<std::string_view> BashPrefix(std::string_view tool) {
std::string_view body = tool;
if (body.ends_with('\n')) body.remove_suffix(1);
if (body.size() < 9 || !body.starts_with("Bash(") || !body.ends_with(":*)")) return std::nullopt;
const std::string_view prefix = body.substr(5, body.size() - 8);
if (prefix.contains('\n')) return std::nullopt;
return prefix;
}
}
bool Denied(std::string_view tool) {
if (std::ranges::contains(Deny(), tool)) return true;
const std::optional<std::string_view> prefix = BashPrefix(tool);
if (!prefix) return false;
return std::ranges::any_of(Deny(), [&](const std::string& entry) {
const std::optional<std::string_view> denied = BashPrefix(entry);
return denied && (*prefix == *denied || prefix->starts_with(std::format("{} ", *denied)));
});
}
}