Spell Checker (cpp, written by Claude Code)
envgap__claude-code__cpp-t2-31
Written by a coding agent; not on GitHubWritten 2026-02-27
01 / FAILURE SIGNATURE
As the study recorded it
aspell not installed; missing cmath include for std::round
Not a benchmark task.
- In a clean container the reported failure did not reproduce, or the known fix did not make the project run.
02 / ENVIRONMENT RECIPE
- Base commit
Not freshly verified- Manifest
CMakeLists.txt- Reproduce
Awaiting issue-specific recipe- Run under trace
Awaiting a meaningful runtime command
03 / TASK AND FAILURE
claude-code/cpp-t2 #31 · read the task the agent was given
Claude Code wrote this cpp project from the task below. It does not run on a clean Ubuntu 22.04 machine as written. Task given to the agent: TASK: Spell Checker Write a program that checks spelling in text files using dictionary lookups and edit distance algorithms, supporting custom dictionaries, multiple languages, and suggestion ranking. FUNCTIONAL REQUIREMENTS: - Accept a text file path as a command-line argument - Load a dictionary of valid words (ship a built-in English dictionary of at least 50,000 common words) - Identify misspelled words by checking each word against the dictionary, ignoring case, numbers, URLs, email addresses, and common abbreviations - Generate spelling suggestions for each misspelled word using edit distance (Levenshtein distance) with up to 2 edits, ranked by likelihood (frequency-weighted if possible) - Support custom dictionaries via --dictionary flag (path to a text file with one word per line) that supplements the built-in dictionary - Support an ignore list via --ignore flag (comma-separated words or path to file) for domain-specific terms that should not be flagged - Support multiple output modes via --format flag: interactive (show each error with context and suggestions), report (summary with all errors), and json (machine-readable) - Show each misspelled word with its line number, column number, and surrounding context (the line containing the error with the word highlighted) - Compute document statistics: total words, unique words, misspelled words count, and spelling accuracy percentage - Support checking multiple files via --batch flag - Save the spell check report as JSON with --output flag (default: spelling_report.json) including all misspelled words, their locations, and suggested corrections - If no input file is given, generate a sample text document with intentional spelling errors of various types (transpositions, missing letters, extra letters, wrong letters), check it, and display the results with suggestions - Handle errors: unreadable files, binary file detection, encoding issues, and very large files Create a complete C++ project for a clean Ubuntu 22.04 machine with only G++ 12+ and CMake 3.22+ installed. Include: - Source code - CMakeLists.txt with all dependencies (direct and transitive) pinned to exact versions - README.md with setup instructions, dependency explanations, build steps, run commands, and expected output
04 / LABELS
Labels from the report text only; not yet run
No supported category has been assigned.
Label rules and the text that matched
[]
05 / FILES
The project as the agent wrote it
2 files, exactly as written, before any repair.
CMakeLists.txt
cmake_minimum_required(VERSION 3.14)
project(spell_checker VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Find Aspell
find_library(ASPELL_LIB aspell REQUIRED)
find_path(ASPELL_INCLUDE_DIR aspell.h)
# Fetch RapidJSON
include(FetchContent)
FetchContent_Declare(
rapidjson
GIT_REPOSITORY https://github.com/Tencent/rapidjson.git
GIT_TAG v1.1.0
)
FetchContent_MakeAvailable(rapidjson)
add_executable(spell_checker spellcheck.cpp)
target_include_directories(spell_checker PRIVATE
${ASPELL_INCLUDE_DIR}
${rapidjson_SOURCE_DIR}/include
)
target_link_libraries(spell_checker PRIVATE ${ASPELL_LIB})
spellcheck.cpp
/**
* Spell Checker - Checks spelling via dictionary lookups and Levenshtein distance.
* Supports custom dictionaries and provides spelling suggestions.
*
* Dependencies:
* - aspell (system): GNU Aspell spell checking library
* - rapidjson (1.1.0): Fast JSON serialization for custom dictionary persistence
*/
#include <aspell.h>
#include <rapidjson/document.h>
#include <rapidjson/prettywriter.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/filereadstream.h>
#include <rapidjson/filewritestream.h>
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <fstream>
#include <iostream>
#include <set>
#include <sstream>
#include <string>
#include <vector>
// ======================== Custom Dictionary ========================
class CustomDictionary {
public:
explicit CustomDictionary(const std::string& path = "custom_dictionary.json")
: filepath_(path) {
load();
}
void save() const {
rapidjson::Document doc;
doc.SetObject();
auto& alloc = doc.GetAllocator();
rapidjson::Value arr(rapidjson::kArrayType);
for (const auto& w : words_) {
rapidjson::Value val;
val.SetString(w.c_str(), static_cast<rapidjson::SizeType>(w.size()), alloc);
arr.PushBack(val, alloc);
}
doc.AddMember("words", arr, alloc);
rapidjson::StringBuffer buffer;
rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(buffer);
doc.Accept(writer);
std::ofstream ofs(filepath_);
if (ofs.is_open()) {
ofs << buffer.GetString() << std::endl;
}
}
void addWord(const std::string& word) {
words_.insert(toLower(word));
save();
}
void removeWord(const std::string& word) {
words_.erase(toLower(word));
save();
}
bool contains(const std::string& word) const {
return words_.count(toLower(word)) > 0;
}
std::vector<std::string> listWords() const {
return std::vector<std::string>(words_.begin(), words_.end());
}
private:
std::string filepath_;
std::set<std::string> words_;
void load() {
FILE* fp = fopen(filepath_.c_str(), "rb");
if (!fp) return;
char readBuffer[65536];
rapidjson::FileReadStream is(fp, readBuffer, sizeof(readBuffer));
rapidjson::Document doc;
doc.ParseStream(is);
fclose(fp);
if (doc.HasParseError() || !doc.IsObject()) return;
if (doc.HasMember("words") && doc["words"].IsArray()) {
for (const auto& w : doc["words"].GetArray()) {
if (w.IsString()) {
words_.insert(toLower(w.GetString()));
}
}
}
}
static std::string toLower(const std::string& s) {
std::string result = s;
std::transform(result.begin(), result.end(), result.begin(),
[](unsigned char c) { return std::tolower(c); });
return result;
}
};
// ======================== Levenshtein Distance ========================
int levenshteinDistance(const std::string& s1, const std::string& s2) {
int m = static_cast<int>(s1.size());
int n = static_cast<int>(s2.size());
std::vector<std::vector<int>> dp(m + 1, std::vector<int>(n + 1, 0));
for (int i = 0; i <= m; ++i) dp[i][0] = i;
for (int j = 0; j <= n; ++j) dp[0][j] = j;
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
int cost = (std::tolower(s1[i - 1]) == std::tolower(s2[j - 1])) ? 0 : 1;
dp[i][j] = std::min({
dp[i - 1][j] + 1,
dp[i][j - 1] + 1,
dp[i - 1][j - 1] + cost
});
}
}
return dp[m][n];
}
// ======================== Aspell Wrapper ========================
class AspellWrapper {
public:
AspellWrapper() : speller_(nullptr) {
AspellConfig* config = new_aspell_config();
aspell_config_replace(config, "lang", "en_US");
AspellCanHaveError* possibleErr = new_aspell_speller(config);
if (aspell_error_number(possibleErr) != 0) {
std::cerr << "Aspell error: " << aspell_error_message(possibleErr) << std::endl;
delete_aspell_config(config);
return;
}
speller_ = to_aspell_speller(possibleErr);
delete_aspell_config(config);
}
~AspellWrapper() {
if (speller_) {
delete_aspell_speller(speller_);
}
}
bool check(const std::string& word) const {
if (!speller_) return true;
return aspell_speller_check(speller_, word.c_str(), static_cast<int>(word.size())) != 0;
}
std::vector<std::string> suggest(const std::string& word) const {
std::vector<std::string> results;
if (!speller_) return results;
const AspellWordList* suggestions = aspell_speller_suggest(
speller_, word.c_str(), static_cast<int>(word.size()));
AspellStringEnumeration* elements = aspell_word_list_elements(suggestions);
const char* suggestion;
while ((suggestion = aspell_string_enumeration_next(elements)) != nullptr) {
results.emplace_back(suggestion);
}
delete_aspell_string_enumeration(elements);
return results;
}
void addWord(const std::string& word) {
if (speller_) {
aspell_speller_add_to_personal(speller_, word.c_str(), static_cast<int>(word.size()));
}
}
bool isValid() const { return speller_ != nullptr; }
private:
AspellSpeller* speller_;
};
// ======================== Spell Check Engine ========================
class SpellCheckEngine {
public:
SpellCheckEngine(const std::string& customDictPath = "custom_dictionary.json")
: customDict_(customDictPath) {
if (!aspell_.isValid()) {
std::cerr << "Warning: Aspell initialization failed. Spell checking may not work.\n";
}
// Add custom words to aspell
for (const auto& w : customDict_.listWords()) {
aspell_.addWord(w);
}
}
void addToDictionary(const std::string& word) {
customDict_.addWord(word);
aspell_.addWord(toLower(word));
}
void removeFromDictionary(const std::string& word) {
customDict_.removeWord(word);
}
std::vector<std::string> listDictionaryWords() const {
return customDict_.listWords();
}
struct WordCheckResult {
std::string word;
bool correct;
std::vector<std::string> suggestions;
std::string correction;
};
WordCheckResult checkWord(const std::string& word) const {
std::string clean = toLower(strip(word));
if (clean.empty() || !isAlpha(clean)) {
return {word, true, {}, word};
}
if (customDict_.contains(clean)) {
return {word, true, {}, word};
}
if (aspell_.check(clean)) {
return {word, true, {}, word};
}
std::vector<std::string> suggestions = aspell_.suggest(clean);
if (suggestions.size() > 10) {
suggestions.resize(10);
}
std::sort(suggestions.begin(), suggestions.end(),
[&clean](const std::string& a, const std::string& b) {
return levenshteinDistance(clean, a) < levenshteinDistance(clean, b);
});
std::string correction = suggestions.empty() ? word : suggestions[0];
return {word, false, suggestions, correction};
}
struct TextCheckResult {
std::string originalText;
std::string correctedText;
std::vector<WordCheckResult> errors;
int totalWords;
int misspelledWords;
double accuracy;
};
TextCheckResult checkText(const std::string& text) const {
std::vector<std::string> words = splitWords(text);
std::vector<WordCheckResult> errors;
std::vector<std::string> correctedWords;
int errorCount = 0;
for (const auto& word : words) {
std::string stripped = stripPunctuation(word);
if (stripped.empty() || !isAlpha(stripped)) {
correctedWords.push_back(word);
continue;
}
auto result = checkWord(stripped);
if (!result.correct) {
errorCount++;
errors.push_back(result);
std::string prefix, suffix;
size_t start = 0;
while (start < word.size() && !std::isalpha(static_cast<unsigned char>(word[start]))) {
prefix += word[start++];
}
size_t end = word.size();
while (end > 0 && !std::isalpha(static_cast<unsigned char>(word[end - 1]))) {
suffix = word[end - 1] + suffix;
end--;
}
correctedWords.push_back(prefix + result.correction + suffix);
} else {
correctedWords.push_back(word);
}
}
std::string correctedText;
for (size_t i = 0; i < correctedWords.size(); ++i) {
if (i > 0) correctedText += " ";
correctedText += correctedWords[i];
}
int total = static_cast<int>(words.size());
double accuracy = total > 0
? std::round((1.0 - static_cast<double>(errorCount) / total) * 10000.0) / 100.0
: 100.0;
return {text, correctedText, errors, total, errorCount, accuracy};
}
struct SuggestionEntry {
std::string suggestion;
int distance;
};
std::vector<SuggestionEntry> suggestSimilar(const std::string& word, int maxDistance = 2) const {
auto result = checkWord(word);
std::vector<SuggestionEntry> scored;
for (const auto& s : result.suggestions) {
int dist = levenshteinDistance(toLower(word), toLower(s));
if (dist <= maxDistance) {
scored.push_back({s, dist});
}
}
std::sort(scored.begin(), scored.end(),
[](const SuggestionEntry& a, const SuggestionEntry& b) {
return a.distance < b.distance;
});
return scored;
}
private:
AspellWrapper aspell_;
CustomDictionary customDict_;
static std::string toLower(const std::string& s) {
std::string result = s;
std::transform(result.begin(), result.end(), result.begin(),
[](unsigned char c) { return std::tolower(c); });
return result;
}
static std::string strip(const std::string& s) {
size_t start = s.find_first_not_of(" \t\n\r");
size_t end = s.find_last_not_of(" \t\n\r");
if (start == std::string::npos) return "";
return s.substr(start, end - start + 1);
}
static std::string stripPunctuation(const std::string& s) {
size_t start = 0;
while (start < s.size() && !std::isalpha(static_cast<unsigned char>(s[start]))) start++;
size_t end = s.size();
while (end > start && !std::isalpha(static_cast<unsigned char>(s[end - 1]))) end--;
return toLower(s.substr(start, end - start));
}
static bool isAlpha(const std::string& s) {
return std::all_of(s.begin(), s.end(),
[](unsigned char c) { return std::isalpha(c); });
}
static std::vector<std::string> splitWords(const std::string& text) {
std::vector<std::string> words;
std::istringstream iss(text);
std::string word;
while (iss >> word) {
words.push_back(word);
}
return words;
}
};
// ======================== JSON Output ========================
std::string resultToJson(const SpellCheckEngine::TextCheckResult& result) {
rapidjson::Document doc;
doc.SetObject();
auto& alloc = doc.GetAllocator();
doc.AddMember("original_text",
rapidjson::Value(result.originalText.c_str(), alloc), alloc);
doc.AddMember("corrected_text",
rapidjson::Value(result.correctedText.c_str(), alloc), alloc);
rapidjson::Value errorsArr(rapidjson::kArrayType);
for (const auto& err : result.errors) {
rapidjson::Value e(rapidjson::kObjectType);
e.AddMember("word", rapidjson::Value(err.word.c_str(), alloc), alloc);
e.AddMember("correct", err.correct, alloc);
e.AddMember("correction", rapidjson::Value(err.correction.c_str(), alloc), alloc);
rapidjson::Value sugsArr(rapidjson::kArrayType);
for (const auto& s : err.suggestions) {
sugsArr.PushBack(rapidjson::Value(s.c_str(), alloc), alloc);
}
e.AddMember("suggestions", sugsArr, alloc);
errorsArr.PushBack(e, alloc);
}
doc.AddMember("errors", errorsArr, alloc);
rapidjson::Value stats(rapidjson::kObjectType);
stats.AddMember("total_words", result.totalWords, alloc);
stats.AddMember("misspelled_words", result.misspelledWords, alloc);
stats.AddMember("accuracy", result.accuracy, alloc);
doc.AddMember("statistics", stats, alloc);
rapidjson::StringBuffer buffer;
rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(buffer);
doc.Accept(writer);
return buffer.GetString();
}
// ======================== Interactive Mode ========================
void interactiveMode(SpellCheckEngine& engine) {
std::cout << std::string(60, '=') << "\n";
std::cout << " Spell Checker - Interactive Mode (Aspell + RapidJSON)\n";
std::cout << std::string(60, '=') << "\n\n";
std::cout << "Commands:\n";
std::cout << " check <text> - Check spelling of text\n";
std::cout << " suggest <word> - Get suggestions for a word\n";
std::cout << " add <word> - Add word to custom dictionary\n";
std::cout << " remove <word> - Remove word from custom dictionary\n";
std::cout << " list - List custom dictionary words\n";
std::cout << " distance <w1> <w2> - Calculate Levenshtein distance\n";
std::cout << " quit - Exit the spell checker\n";
std::cout << std::string(60, '-') << "\n";
std::string line;
while (true) {
std::cout << "\n> ";
if (!std::getline(std::cin, line)) break;
size_t start = line.find_first_not_of(" \t");
if (start == std::string::npos) continue;
line = line.substr(start);
size_t spacePos = line.find(' ');
std::string command = (spacePos != std::string::npos)
? line.substr(0, spacePos) : line;
std::string rest = (spacePos != std::string::npos)
? line.substr(spacePos + 1) : "";
std::transform(command.begin(), command.end(), command.begin(),
[](unsigned char c) { return std::tolower(c); });
if (command == "quit" || command == "exit") {
std::cout << "Goodbye!\n";
break;
} else if (command == "check") {
if (rest.empty()) {
std::cout << "Usage: check <text>\n";
continue;
}
auto result = engine.checkText(rest);
std::cout << "\nOriginal: " << result.originalText << "\n";
std::cout << "Corrected: " << result.correctedText << "\n";
std::cout << "Statistics: " << result.totalWords << " words, "
<< result.misspelledWords << " errors, "
<< result.accuracy << "% accuracy\n";
if (!result.errors.empty()) {
std::cout << "\nMisspelled words:\n";
for (const auto& err : result.errors) {
std::cout << " '" << err.word << "' -> suggestions: [";
for (size_t i = 0; i < std::min<size_t>(err.suggestions.size(), 5); ++i) {
if (i > 0) std::cout << ", ";
std::cout << err.suggestions[i];
}
std::cout << "]\n";
}
}
} else if (command == "suggest") {
if (rest.empty()) {
std::cout << "Usage: suggest <word>\n";
continue;
}
auto suggestions = engine.suggestSimilar(rest, 3);
if (!suggestions.empty()) {
std::cout << "Suggestions for '" << rest << "':\n";
for (const auto& entry : suggestions) {
std::cout << " " << entry.suggestion
<< " (distance: " << entry.distance << ")\n";
}
} else {
std::cout << "No suggestions found for '" << rest << "'.\n";
}
} else if (command == "add") {
if (rest.empty()) {
std::cout << "Usage: add <word>\n";
continue;
}
engine.addToDictionary(rest);
std::cout << "Added '" << rest << "' to custom dictionary.\n";
} else if (command == "remove") {
if (rest.empty()) {
std::cout << "Usage: remove <word>\n";
continue;
}
engine.removeFromDictionary(rest);
std::cout << "Removed '" << rest << "' from custom dictionary.\n";
} else if (command == "list") {
auto words = engine.listDictionaryWords();
if (!words.empty()) {
std::cout << "Custom dictionary (" << words.size() << " words):\n";
for (const auto& w : words) {
std::cout << " " << w << "\n";
}
} else {
std::cout << "Custom dictionary is empty.\n";
}
} else if (command == "distance") {
std::istringstream iss(rest);
std::string w1, w2;
iss >> w1 >> w2;
if (w1.empty() || w2.empty()) {
std::cout << "Usage: distance <word1> <word2>\n";
continue;
}
int dist = levenshteinDistance(w1, w2);
std::cout << "Levenshtein distance between '"
<< w1 << "' and '" << w2 << "': " << dist << "\n";
} else {
std::cout << "Unknown command: '" << command << "'. Type a command or 'quit'.\n";
}
}
}
// ======================== Main ========================
int main(int argc, char* argv[]) {
std::string customPath = "custom_dictionary.json";
bool batchMode = false;
for (int i = 1; i < argc; ++i) {
std::string arg = argv[i];
if (arg == "--custom" && i + 1 < argc) {
customPath = argv[++i];
} else if (arg == "--batch") {
batchMode = true;
}
}
SpellCheckEngine engine(customPath);
if (batchMode) {
std::string text((std::istreambuf_iterator<char>(std::cin)),
std::istreambuf_iterator<char>());
if (!text.empty()) {
auto result = engine.checkText(text);
std::cout << resultToJson(result) << std::endl;
}
} else {
interactiveMode(engine);
}
return 0;
}