← All tasks
cppclaude-code/cpp-t2 #31Not a task: not reproduced

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;
}