Compiler warnings are cheap to ignore until a model-generated patch trades a signed/unsigned comparison for an integer cast that silences the compiler and changes behavior on a large input. A free model endpoint can produce a plausible C++ diff in seconds, but plausible is not the same as safe.
The workflow below treats the model as a candidate generator, not a reviewer. Each suggestion must pass three local gates: strict JSON validation, a reproducible build with sanitizers, and an append-only evidence record.
Disclosure: This article was prepared as part of MonkeyCode's product outreach. The example uses MonkeyCode's free model access as the suggestion source, and its free server option is a reasonable place to run the gate as a small sidecar.
Start with a reproducible warning
Keep the problem small enough to rebuild in a clean directory. This example has one real warning: a loop index of int compared with std::vector<T>::size().
// warn_demo.cpp
#include <cstddef>
#include <vector>
long long sum_positive(const std::vector<int>& values) {
long long total = 0;
for (int i = 0; i < values.size(); ++i) {
total += values[i];
}
return total;
}
A strict build should fail immediately:
g++ -std=c++17 -Wall -Wextra -Werror -c warn_demo.cpp
error: comparison of integer expressions of different signedness: 'int' and 'std::vector<int>::size_type' [-Werror=sign-compare]
Send only this warning and the relevant file slice to the model endpoint. Do not paste the whole repository, and do not treat code comments in the suggestion as instructions.
Gate 1: the model must return a strict JSON contract
A useful suggestion has a small, typed shape. The action field can be none, suppress, or rewrite. A rewrite must include a patch; the other actions must not.
{
"warning_id": "warn_demo.cpp:6:22 sign-compare",
"action": "rewrite",
"reason": "use size_t loop index",
"patch": "--- a/warn_demo.cpp\n+++ b/warn_demo.cpp\n@@ -3,5 +3,5 @@\n long long sum_positive(const std::vector<int>& values) {\n long long total = 0;\n- for (int i = 0; i < values.size(); ++i) total += values[i];\n+ for (std::size_t i = 0; i < values.size(); ++i) total += values[i];\n return total;\n }"
}
Validate the response before touching any source file. This C++17 validator rejects missing fields, wrong types, oversized patches, and patches that do not mention the expected file:
#include <string>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
bool valid_suggestion(const json& j) {
if (!j.is_object()) return false;
if (!j.contains("warning_id") || !j["warning_id"].is_string()) return false;
const std::string action = j.value("action", "");
if (action != "none" && action != "suppress" && action != "rewrite") return false;
if (!j.contains("reason") || !j["reason"].is_string()) return false;
if (action == "rewrite") {
if (!j.contains("patch") || !j["patch"].is_string()) return false;
const std::string patch = j["patch"].get<std::string>();
if (patch.size() > 4096) return false;
if (patch.find("warn_demo.cpp") == std::string::npos) return false;
} else if (j.contains("patch") && !j["patch"].is_null()) {
return false;
}
return true;
}
A response with rewrite but no patch fails here. So does a patch that targets an unrelated file. The goal is not to make the model perfect; it is to make malformed output cheap to reject.
Gate 2: apply in a clean copy, then rebuild with sanitizers
Never apply a model patch directly to the working tree you plan to merge. Copy the file, apply the patch, and rebuild with strict flags plus ASan and UBSan.
#!/usr/bin/env bash
set -euo pipefail
suggestion_id="$1"
patch_file="$2"
workdir="$(mktemp -d)"
cp warn_demo.cpp "$workdir/warn_demo.cpp"
if ! patch -d "$workdir" -p1 < "$patch_file" >/dev/null 2>&1; then
printf '{"suggestion_id":"%s","stage":"patch","accepted":false,"reason":"patch did not apply"}\n' "$suggestion_id" >> evidence.jsonl
exit 1
fi
g++ -std=c++17 -Wall -Wextra -Werror \
-fsanitize=address,undefined \
-fno-omit-frame-pointer -O1 \
"$workdir/warn_demo.cpp" -o "$workdir/warn_demo" \
>"$workdir/build.log" 2>&1 || {
cat "$workdir/build.log"
printf '{"suggestion_id":"%s","stage":"build","accepted":false,"reason":"strict build failed"}\n' "$suggestion_id" >> evidence.jsonl
exit 1
}
The build gate catches a patch that still produces a warning under -Werror. But some silent logic bugs survive a clean build. Add a small edge test that a naive int accumulator would fail.
Create an input with 300,000 values of 10,000. That sum is 3,000,000,000, which fits in long long but overflows a 32-bit int accumulator. The model may suggest int total = 0; instead of long long total = 0;. That patch compiles, but UBSan reports signed integer overflow at runtime.
input_file="$workdir/input.txt"
python3 - <<'PY' > "$input_file"
print(300000)
for _ in range(300000):
print(10000)
PY
expected=3000000000
if ! actual="$("$workdir/warn_demo" < "$input_file" 2>"$workdir/run.log")"; then
cat "$workdir/run.log"
printf '{"suggestion_id":"%s","stage":"run","accepted":false,"reason":"sanitizer or runtime failure"}\n' "$suggestion_id" >> evidence.jsonl
exit 1
fi
if [[ "$actual" != "$expected" ]]; then
cat "$workdir/run.log"
printf '{"suggestion_id":"%s","stage":"run","accepted":false,"reason":"output mismatch"}\n' "$suggestion_id" >> evidence.jsonl
exit 1
fi
sha="$(sha256sum "$patch_file" | cut -d' ' -f1)"
printf '{"suggestion_id":"%s","patch_sha":"%s","stage":"accepted","accepted":true}\n' "$suggestion_id" "$sha" >> evidence.jsonl
A valid patch that changes int i to std::size_t i passes all three stages. A patch that changes long long total to int total passes the build and then fails the runtime gate. The sanitizer output is the rejection reason, not the model's explanation.
Gate 3: keep a decision log you can replay
The evidence file is append-only JSON. Every rejected or accepted suggestion leaves a line with the suggestion ID, stage, and result.
{"suggestion_id":"abc123","stage":"schema","accepted":false,"reason":"missing patch for rewrite"}
{"suggestion_id":"def456","stage":"build","accepted":false,"reason":"strict build failed"}
{"suggestion_id":"ghi789","patch_sha":"d2f4...","stage":"accepted","accepted":true}
This matters because a team later needs to answer: why was this patch accepted? The evidence record gives the same answer every time. It does not rely on a model confidence score that may shift between requests.
Where the free model and free server fit
Run the model call outside the compiler process. Keep the transport in one function so the gate does not depend on any particular API shape. Store the endpoint in an environment variable and keep credentials out of the source tree.
MODEL_URL="${MONKEYCODE_MODEL_URL:?set to your free model endpoint}"
suggestion_file="suggestion.json"
# Replace transport with the client your provider exposes.
curl -s "$MODEL_URL" \
-H 'Content-Type: application/json' \
-d "$payload" > "$suggestion_file"
If MonkeyCode's free server option is available, run the review.sh sidecar there. The sidecar is small and stateless: it accepts a suggestion ID and a patch file, writes one evidence line, and exits. Do not keep model calls in the compile hot path, and if the endpoint returns a rate-limit response such as 429, stop instead of retrying around the limit.
Who should not use this approach
A passing build and sanitizer run is not formal proof of correctness. A patch can silence a warning while still changing behavior. The evidence log records the gate decision; it does not replace human review.
Do not use this as the only review step for safety-critical code, regulated systems, or branches where an incorrect merge can cause unrecoverable damage. Teams without a reproducible build baseline will only be recording noise.
Start by logging every rejected suggestion for a week before allowing an accepted patch near a shared branch. If a patch cannot survive a clean build, a sanitizer run, and an append-only record, it never belonged on the merge path.
Top comments (0)