← All tasks
javascriptclaude-code/javascript-t1 #31Not a task: already works

Spell Checker (javascript, written by Claude Code)

envgap__claude-code__javascript-t1-31

Written by a coding agent; not on GitHubWritten 2026-02-27

01 / FAILURE SIGNATURE

As the study recorded it

No identifying execution failure has been captured.
Not a benchmark task.
  • The project already builds and runs before the fix, so there is nothing to repair.

02 / ENVIRONMENT RECIPE

Base commit
Not freshly verified
Manifest
package.json
Reproduce
Awaiting issue-specific recipe
Run under trace
Awaiting a meaningful runtime command

03 / TASK AND FAILURE

claude-code/javascript-t1 #31 · read the task the agent was given
Claude Code wrote this javascript project from the task below. It installed and ran 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 JavaScript project for a clean Ubuntu 22.04 machine with only Node.js 20+ (LTS) installed. Include:
- Source code
- package.json 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.

package.json
{
  "name": "spell-checker",
  "version": "1.0.0",
  "description": "Spell checker using dictionary lookups and Levenshtein distance with custom dictionary support",
  "main": "spellcheck.js",
  "scripts": {
    "start": "node spellcheck.js"
  },
  "keywords": ["spellcheck", "levenshtein", "dictionary", "nspell"],
  "license": "MIT",
  "dependencies": {
    "nspell": "2.1.5",
    "chalk": "4.1.2",
    "dictionary-en": "^3.0.0"
  }
}
spellcheck.js
/**
 * Spell Checker - Checks spelling via dictionary lookups and Levenshtein distance.
 * Supports custom dictionaries and provides spelling suggestions.
 *
 * Dependencies:
 *   - nspell (2.1.5): Hunspell-compatible spell checking for Node.js
 *   - chalk (4.1.2): Terminal string styling for colored output
 */

const nspell = require("nspell");
const chalk = require("chalk");
const fs = require("fs");
const path = require("path");
const readline = require("readline");

// Load English dictionary for nspell
const affPath = path.join(
  __dirname,
  "node_modules",
  "dictionary-en",
  "index.aff"
);
const dicPath = path.join(
  __dirname,
  "node_modules",
  "dictionary-en",
  "index.dic"
);

/**
 * Custom dictionary manager with JSON persistence.
 */
class CustomDictionary {
  constructor(filePath) {
    this.filePath = filePath || path.join(__dirname, "custom_dictionary.json");
    this.words = new Set();
    this._load();
  }

  _load() {
    try {
      if (fs.existsSync(this.filePath)) {
        const data = JSON.parse(fs.readFileSync(this.filePath, "utf-8"));
        if (data.words && Array.isArray(data.words)) {
          data.words.forEach((w) => this.words.add(w.toLowerCase()));
        }
      }
    } catch (err) {
      // Start with empty dictionary on error
    }
  }

  save() {
    const data = { words: Array.from(this.words).sort() };
    fs.writeFileSync(this.filePath, JSON.stringify(data, null, 2), "utf-8");
  }

  add(word) {
    this.words.add(word.toLowerCase());
    this.save();
  }

  remove(word) {
    this.words.delete(word.toLowerCase());
    this.save();
  }

  contains(word) {
    return this.words.has(word.toLowerCase());
  }

  list() {
    return Array.from(this.words).sort();
  }
}

/**
 * Calculate Levenshtein distance between two strings.
 */
function levenshteinDistance(a, b) {
  const m = a.length;
  const n = b.length;
  const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));

  for (let i = 0; i <= m; i++) dp[i][0] = i;
  for (let j = 0; j <= n; j++) dp[0][j] = j;

  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      const cost = a[i - 1] === b[j - 1] ? 0 : 1;
      dp[i][j] = Math.min(
        dp[i - 1][j] + 1, // deletion
        dp[i][j - 1] + 1, // insertion
        dp[i - 1][j - 1] + cost // substitution
      );
    }
  }

  return dp[m][n];
}

/**
 * Spell checking engine using nspell with custom dictionary support.
 */
class SpellCheckEngine {
  constructor(customDictPath) {
    this.customDict = new CustomDictionary(customDictPath);
    this.spell = null;
  }

  async initialize() {
    try {
      const aff = fs.readFileSync(affPath, "utf-8");
      const dic = fs.readFileSync(dicPath, "utf-8");
      this.spell = nspell(aff, dic);

      // Add custom dictionary words to nspell
      for (const word of this.customDict.words) {
        this.spell.add(word);
      }
    } catch (err) {
      // Try loading dictionary-en via require
      try {
        const dictionary = require("dictionary-en");
        await new Promise((resolve, reject) => {
          dictionary((err, dict) => {
            if (err) reject(err);
            this.spell = nspell(dict);
            for (const word of this.customDict.words) {
              this.spell.add(word);
            }
            resolve();
          });
        });
      } catch (loadErr) {
        console.error(
          chalk.red("Error: Could not load English dictionary."),
          chalk.yellow(
            "Install it with: npm install dictionary-en"
          )
        );
        process.exit(1);
      }
    }
  }

  addToDictionary(word) {
    this.customDict.add(word);
    if (this.spell) {
      this.spell.add(word.toLowerCase());
    }
  }

  removeFromDictionary(word) {
    this.customDict.remove(word);
    if (this.spell) {
      this.spell.remove(word.toLowerCase());
    }
  }

  checkWord(word) {
    const cleanWord = word.trim().toLowerCase();
    if (!cleanWord || !/^[a-z]+$/i.test(cleanWord)) {
      return { word, correct: true, suggestions: [], correction: word };
    }

    // Check custom dictionary
    if (this.customDict.contains(cleanWord)) {
      return { word, correct: true, suggestions: [], correction: word };
    }

    if (this.spell && this.spell.correct(cleanWord)) {
      return { word, correct: true, suggestions: [], correction: word };
    }

    // Get suggestions from nspell
    let suggestions = [];
    if (this.spell) {
      suggestions = this.spell.suggest(cleanWord).slice(0, 10);
    }

    // Sort suggestions by Levenshtein distance
    suggestions.sort(
      (a, b) =>
        levenshteinDistance(cleanWord, a.toLowerCase()) -
        levenshteinDistance(cleanWord, b.toLowerCase())
    );

    const correction = suggestions.length > 0 ? suggestions[0] : word;

    return {
      word,
      correct: false,
      suggestions,
      correction,
    };
  }

  checkText(text) {
    const words = text.split(/\s+/);
    const errors = [];
    const correctedWords = [];
    let errorCount = 0;

    for (const word of words) {
      const stripped = word.replace(/^[^a-zA-Z]+|[^a-zA-Z]+$/g, "").toLowerCase();
      if (!stripped) {
        correctedWords.push(word);
        continue;
      }

      const result = this.checkWord(stripped);
      if (!result.correct) {
        errorCount++;
        errors.push(result);

        // Preserve surrounding punctuation
        const prefixMatch = word.match(/^([^a-zA-Z]*)/);
        const suffixMatch = word.match(/([^a-zA-Z]*)$/);
        const prefix = prefixMatch ? prefixMatch[1] : "";
        const suffix = suffixMatch ? suffixMatch[1] : "";
        correctedWords.push(prefix + result.correction + suffix);
      } else {
        correctedWords.push(word);
      }
    }

    const totalWords = words.filter((w) => /[a-zA-Z]/.test(w)).length;
    const accuracy =
      totalWords > 0
        ? Math.round((1 - errorCount / totalWords) * 10000) / 100
        : 100;

    return {
      original_text: text,
      corrected_text: correctedWords.join(" "),
      errors,
      statistics: {
        total_words: totalWords,
        misspelled_words: errorCount,
        accuracy,
      },
    };
  }

  suggestSimilar(word, maxDistance = 2) {
    const result = this.checkWord(word);
    const scored = [];

    for (const suggestion of result.suggestions) {
      const dist = levenshteinDistance(
        word.toLowerCase(),
        suggestion.toLowerCase()
      );
      if (dist <= maxDistance) {
        scored.push({ suggestion, distance: dist });
      }
    }

    scored.sort((a, b) => a.distance - b.distance);
    return scored;
  }
}

/**
 * Run interactive spell checker mode.
 */
async function interactiveMode(engine) {
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });

  console.log(chalk.bold("=".repeat(60)));
  console.log(chalk.bold.blue("  Spell Checker - Interactive Mode"));
  console.log(chalk.bold("=".repeat(60)));
  console.log();
  console.log("Commands:");
  console.log("  " + chalk.green("check <text>") + "       - Check spelling of text");
  console.log("  " + chalk.green("suggest <word>") + "     - Get suggestions for a word");
  console.log("  " + chalk.green("add <word>") + "         - Add word to custom dictionary");
  console.log("  " + chalk.green("remove <word>") + "      - Remove word from custom dictionary");
  console.log("  " + chalk.green("list") + "               - List custom dictionary words");
  console.log(
    "  " + chalk.green("distance <w1> <w2>") + " - Calculate Levenshtein distance"
  );
  console.log("  " + chalk.green("quit") + "               - Exit the spell checker");
  console.log("-".repeat(60));

  const prompt = () => {
    rl.question(chalk.cyan("\n> "), (input) => {
      input = input.trim();
      if (!input) {
        prompt();
        return;
      }

      const spaceIdx = input.indexOf(" ");
      const command = spaceIdx >= 0 ? input.substring(0, spaceIdx).toLowerCase() : input.toLowerCase();
      const rest = spaceIdx >= 0 ? input.substring(spaceIdx + 1).trim() : "";

      switch (command) {
        case "quit":
        case "exit":
          console.log("Goodbye!");
          rl.close();
          return;

        case "check": {
          if (!rest) {
            console.log("Usage: check <text>");
            break;
          }
          const result = engine.checkText(rest);
          console.log(
            "\n" + chalk.white("Original:  ") + result.original_text
          );
          console.log(
            chalk.white("Corrected: ") + chalk.green(result.corrected_text)
          );
          const stats = result.statistics;
          console.log(
            chalk.white("Statistics: ") +
              `${stats.total_words} words, ${stats.misspelled_words} errors, ${stats.accuracy}% accuracy`
          );
          if (result.errors.length > 0) {
            console.log(chalk.yellow("\nMisspelled words:"));
            for (const err of result.errors) {
              const sugs = err.suggestions.slice(0, 5).join(", ");
              console.log(
                `  ${chalk.red("'" + err.word + "'")} -> suggestions: [${chalk.green(sugs)}]`
              );
            }
          }
          break;
        }

        case "suggest": {
          if (!rest) {
            console.log("Usage: suggest <word>");
            break;
          }
          const suggestions = engine.suggestSimilar(rest, 3);
          if (suggestions.length > 0) {
            console.log(`Suggestions for '${rest}':`);
            for (const entry of suggestions) {
              console.log(
                `  ${chalk.green(entry.suggestion)} (distance: ${entry.distance})`
              );
            }
          } else {
            console.log(chalk.yellow(`No suggestions found for '${rest}'.`));
          }
          break;
        }

        case "add":
          if (!rest) {
            console.log("Usage: add <word>");
            break;
          }
          engine.addToDictionary(rest);
          console.log(chalk.green(`Added '${rest}' to custom dictionary.`));
          break;

        case "remove":
          if (!rest) {
            console.log("Usage: remove <word>");
            break;
          }
          engine.removeFromDictionary(rest);
          console.log(chalk.yellow(`Removed '${rest}' from custom dictionary.`));
          break;

        case "list": {
          const words = engine.customDict.list();
          if (words.length > 0) {
            console.log(`Custom dictionary (${words.length} words):`);
            for (const w of words) {
              console.log(`  ${chalk.blue(w)}`);
            }
          } else {
            console.log("Custom dictionary is empty.");
          }
          break;
        }

        case "distance": {
          const distParts = rest.split(/\s+/);
          if (distParts.length < 2) {
            console.log("Usage: distance <word1> <word2>");
            break;
          }
          const dist = levenshteinDistance(distParts[0], distParts[1]);
          console.log(
            `Levenshtein distance between '${distParts[0]}' and '${distParts[1]}': ${chalk.bold(String(dist))}`
          );
          break;
        }

        default:
          console.log(
            chalk.red(`Unknown command: '${command}'. Type a command or 'quit'.`)
          );
      }

      prompt();
    });
  };

  prompt();
}

/**
 * Main entry point.
 */
async function main() {
  const customDictPath = process.argv[2] || null;
  const engine = new SpellCheckEngine(customDictPath);
  await engine.initialize();

  if (process.stdin.isTTY) {
    await interactiveMode(engine);
  } else {
    // Batch mode: read from stdin and output JSON
    let input = "";
    process.stdin.setEncoding("utf-8");
    process.stdin.on("data", (chunk) => {
      input += chunk;
    });
    process.stdin.on("end", () => {
      const text = input.trim();
      if (text) {
        const result = engine.checkText(text);
        console.log(JSON.stringify(result, null, 2));
      }
    });
  }
}

main().catch((err) => {
  console.error("Error:", err.message);
  process.exit(1);
});