← All tasks
javascriptcodex/javascript-t1 #33Not a task: already works

Text Diff Tool (javascript, written by Codex)

envgap__codex__javascript-t1-33

Written by a coding agent; not on GitHubWritten 2026-03-03

01 / FAILURE SIGNATURE

As the study recorded it

None
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

codex/javascript-t1 #33 · read the task the agent was given
Codex 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: Text Diff Tool

Write a program that compares two text files and produces a detailed diff showing additions, deletions, and modifications with configurable output formats and context control.

FUNCTIONAL REQUIREMENTS:
- Accept two file paths as command-line arguments (original and modified)
- Compute the longest common subsequence (LCS) based diff to identify added, deleted, and changed lines
- Support multiple output formats via --format flag: unified diff (default, similar to git diff), side-by-side (two-column view), inline (changes marked within lines), and html (visual diff as an HTML page)
- Support configurable context lines around changes via --context flag (default: 3 lines of unchanged context around each change)
- Detect and highlight intra-line changes: when a line is modified, show exactly which words or characters changed within the line
- Support ignoring whitespace differences via --ignore-whitespace flag
- Support ignoring case differences via --ignore-case flag
- Support ignoring blank lines via --ignore-blank-lines flag
- Compute and display diff statistics: total lines in each file, lines added, lines deleted, lines modified, and a similarity percentage
- Support comparing directories via --recursive flag: compare all matching files in two directory trees and report which files are added, deleted, modified, or identical
- Apply color coding in console output: green for additions, red for deletions, yellow for modifications
- Save the diff output to a file via --output flag
- If no input files are given, generate two sample text files (original and modified version with insertions, deletions, modifications, and moved blocks), then compute and display the diff in all supported formats
- Handle errors: binary files (detect and skip with warning), missing files, encoding mismatches, 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

4 files, exactly as written, before any repair.

package-lock.json
{
  "name": "text-diff-tool",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "text-diff-tool",
      "version": "1.0.0",
      "dependencies": {},
      "engines": {
        "node": ">=20.0.0"
      }
    }
  }
}
package.json
{
  "name": "text-diff-tool",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "main": "src/index.js",
  "scripts": {
    "start": "node src/index.js"
  },
  "engines": {
    "node": ">=20.0.0"
  },
  "dependencies": {}
}
README.md
# Text Diff Tool (JavaScript)

Compares two text files using an LCS-based diff and outputs unified, side-by-side, inline, or HTML diffs.

## Requirements
- Ubuntu 22.04
- Node.js 20+

## Dependencies
- No external runtime dependencies

## Setup
```bash
npm install
```

## Run
Unified diff:
```bash
npm start -- original.txt modified.txt --format unified --context 3
```

Side-by-side:
```bash
npm start -- original.txt modified.txt --format side-by-side
```

Inline diff with ignore flags:
```bash
npm start -- original.txt modified.txt --format inline --ignore-whitespace --ignore-case
```

HTML output:
```bash
npm start -- original.txt modified.txt --format html --output diff.html
```

Recursive directory comparison:
```bash
npm start -- dir_a dir_b --recursive
```

No input files:
```bash
npm start
```
Generates sample files and displays all supported formats.
src/index.js
import fs from "node:fs";
import path from "node:path";
import process from "node:process";

const ANSI = {
  reset: "\x1b[0m",
  red: "\x1b[31m",
  green: "\x1b[32m",
  yellow: "\x1b[33m"
};

function colorize(text, color) {
  return `${ANSI[color] || ""}${text}${ANSI.reset}`;
}

function parseArgs(argv) {
  const cfg = {
    format: "unified",
    context: 3,
    ignoreWhitespace: false,
    ignoreCase: false,
    ignoreBlankLines: false,
    recursive: false,
    output: null,
    inputs: []
  };
  for (let i = 0; i < argv.length; i += 1) {
    const a = argv[i];
    if (!a.startsWith("--")) {
      cfg.inputs.push(a);
      continue;
    }
    if (a === "--ignore-whitespace") cfg.ignoreWhitespace = true;
    else if (a === "--ignore-case") cfg.ignoreCase = true;
    else if (a === "--ignore-blank-lines") cfg.ignoreBlankLines = true;
    else if (a === "--recursive") cfg.recursive = true;
    else if (a === "--format") cfg.format = argv[++i];
    else if (a === "--context") cfg.context = Number.parseInt(argv[++i], 10);
    else if (a === "--output") cfg.output = argv[++i];
    else throw new Error(`Unknown option: ${a}`);
  }
  if (!["unified", "side-by-side", "inline", "html"].includes(cfg.format)) {
    throw new Error("--format must be one of: unified, side-by-side, inline, html");
  }
  if (!Number.isInteger(cfg.context) || cfg.context < 0) {
    throw new Error("--context must be a non-negative integer");
  }
  return cfg;
}

function maybeNormalize(line, cfg) {
  let x = line;
  if (cfg.ignoreWhitespace) x = x.replace(/\s+/g, " ").trim();
  if (cfg.ignoreCase) x = x.toLowerCase();
  return x;
}

function isBinary(buffer) {
  const n = Math.min(buffer.length, 1024);
  for (let i = 0; i < n; i += 1) if (buffer[i] === 0) return true;
  return false;
}

function readLines(filePath, cfg) {
  const buf = fs.readFileSync(filePath);
  if (isBinary(buf)) return { binary: true, lines: [] };
  if (buf.length > 20 * 1024 * 1024) {
    throw new Error(`Very large file skipped: ${filePath}`);
  }
  const content = buf.toString("utf8");
  let lines = content.replace(/\r\n/g, "\n").split("\n");
  if (cfg.ignoreBlankLines) lines = lines.filter((l) => l.trim() !== "");
  const norm = lines.map((l) => maybeNormalize(l, cfg));
  return { binary: false, lines, norm };
}

function lcsDiff(a, b) {
  const n = a.length;
  const m = b.length;
  const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
  for (let i = n - 1; i >= 0; i -= 1) {
    for (let j = m - 1; j >= 0; j -= 1) {
      dp[i][j] = a[i] === b[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
    }
  }
  const ops = [];
  let i = 0;
  let j = 0;
  while (i < n && j < m) {
    if (a[i] === b[j]) {
      ops.push({ type: "equal", ai: i, bj: j });
      i += 1;
      j += 1;
    } else if (dp[i + 1][j] >= dp[i][j + 1]) {
      ops.push({ type: "del", ai: i, bj: null });
      i += 1;
    } else {
      ops.push({ type: "add", ai: null, bj: j });
      j += 1;
    }
  }
  while (i < n) {
    ops.push({ type: "del", ai: i, bj: null });
    i += 1;
  }
  while (j < m) {
    ops.push({ type: "add", ai: null, bj: j });
    j += 1;
  }
  return ops;
}

function pairModifications(ops) {
  const out = [];
  let i = 0;
  while (i < ops.length) {
    if (ops[i].type !== "del") {
      out.push(ops[i]);
      i += 1;
      continue;
    }
    let di = i;
    while (di < ops.length && ops[di].type === "del") di += 1;
    let ai = di;
    while (ai < ops.length && ops[ai].type === "add") ai += 1;
    if (di < ai) {
      const dels = ops.slice(i, di);
      const adds = ops.slice(di, ai);
      const k = Math.min(dels.length, adds.length);
      for (let t = 0; t < k; t += 1) {
        out.push({ type: "mod", ai: dels[t].ai, bj: adds[t].bj });
      }
      for (let t = k; t < dels.length; t += 1) out.push(dels[t]);
      for (let t = k; t < adds.length; t += 1) out.push(adds[t]);
      i = ai;
    } else {
      out.push(ops[i]);
      i += 1;
    }
  }
  return out;
}

function tokenizeForInline(line) {
  return line.split(/(\s+|[^\w\s]+)/).filter((x) => x !== "");
}

function inlineWordDiff(oldLine, newLine) {
  const a = tokenizeForInline(oldLine);
  const b = tokenizeForInline(newLine);
  const ops = lcsDiff(a, b);
  const oldOut = [];
  const newOut = [];
  for (const op of ops) {
    if (op.type === "equal") {
      oldOut.push(a[op.ai]);
      newOut.push(b[op.bj]);
    } else if (op.type === "del") {
      oldOut.push(`[-${a[op.ai]}-]`);
    } else if (op.type === "add") {
      newOut.push(`[+${b[op.bj]}+]`);
    }
  }
  return { oldLine: oldOut.join(""), newLine: newOut.join("") };
}

function diffStats(ops, aLen, bLen) {
  let added = 0;
  let deleted = 0;
  let modified = 0;
  let equal = 0;
  for (const op of ops) {
    if (op.type === "add") added += 1;
    else if (op.type === "del") deleted += 1;
    else if (op.type === "mod") modified += 1;
    else if (op.type === "equal") equal += 1;
  }
  const similarity = Math.max(aLen, bLen) === 0 ? 100 : (equal / Math.max(aLen, bLen)) * 100;
  return {
    totalOriginal: aLen,
    totalModified: bLen,
    added,
    deleted,
    modified,
    similarityPct: similarity
  };
}

function renderUnified(ops, leftLines, rightLines, context) {
  const changedIdx = [];
  for (let i = 0; i < ops.length; i += 1) {
    if (ops[i].type !== "equal") changedIdx.push(i);
  }
  if (!changedIdx.length) return "No differences.\n";

  const keep = new Set();
  for (const idx of changedIdx) {
    for (let k = Math.max(0, idx - context); k <= Math.min(ops.length - 1, idx + context); k += 1) keep.add(k);
  }
  const out = ["--- original", "+++ modified"];
  let skip = false;
  for (let i = 0; i < ops.length; i += 1) {
    if (!keep.has(i)) {
      if (!skip) out.push("@@ ... @@");
      skip = true;
      continue;
    }
    skip = false;
    const op = ops[i];
    if (op.type === "equal") out.push(` ${leftLines[op.ai]}`);
    else if (op.type === "add") out.push(colorize(`+${rightLines[op.bj]}`, "green"));
    else if (op.type === "del") out.push(colorize(`-${leftLines[op.ai]}`, "red"));
    else if (op.type === "mod") {
      const intra = inlineWordDiff(leftLines[op.ai], rightLines[op.bj]);
      out.push(colorize(`~${intra.oldLine}`, "yellow"));
      out.push(colorize(`~${intra.newLine}`, "yellow"));
    }
  }
  return `${out.join("\n")}\n`;
}

function renderSideBySide(ops, leftLines, rightLines) {
  const leftWidth = 60;
  const out = [];
  for (const op of ops) {
    const left = op.ai === null ? "" : leftLines[op.ai];
    const right = op.bj === null ? "" : rightLines[op.bj];
    if (op.type === "add") out.push(colorize(`${"".padEnd(leftWidth)} | + ${right}`, "green"));
    else if (op.type === "del") out.push(colorize(`${left.padEnd(leftWidth)} | - `, "red"));
    else if (op.type === "mod") {
      const intra = inlineWordDiff(left, right);
      out.push(colorize(`${intra.oldLine.padEnd(leftWidth)} | ~ ${intra.newLine}`, "yellow"));
    } else out.push(`${left.padEnd(leftWidth)} |   ${right}`);
  }
  return `${out.join("\n")}\n`;
}

function renderInline(ops, leftLines, rightLines) {
  const out = [];
  for (const op of ops) {
    if (op.type === "equal") out.push(`  ${leftLines[op.ai]}`);
    else if (op.type === "add") out.push(colorize(`+ ${rightLines[op.bj]}`, "green"));
    else if (op.type === "del") out.push(colorize(`- ${leftLines[op.ai]}`, "red"));
    else {
      const intra = inlineWordDiff(leftLines[op.ai], rightLines[op.bj]);
      out.push(colorize(`~ ${intra.oldLine}`, "yellow"));
      out.push(colorize(`~ ${intra.newLine}`, "yellow"));
    }
  }
  return `${out.join("\n")}\n`;
}

function escapeHtml(text) {
  return text
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#39;");
}

function renderHtml(ops, leftLines, rightLines) {
  const rows = [];
  for (const op of ops) {
    if (op.type === "equal") {
      rows.push(`<tr class="eq"><td>${escapeHtml(leftLines[op.ai])}</td><td>${escapeHtml(rightLines[op.bj])}</td></tr>`);
    } else if (op.type === "add") {
      rows.push(`<tr class="add"><td></td><td>${escapeHtml(rightLines[op.bj])}</td></tr>`);
    } else if (op.type === "del") {
      rows.push(`<tr class="del"><td>${escapeHtml(leftLines[op.ai])}</td><td></td></tr>`);
    } else {
      const intra = inlineWordDiff(leftLines[op.ai], rightLines[op.bj]);
      rows.push(
        `<tr class="mod"><td>${escapeHtml(intra.oldLine)}</td><td>${escapeHtml(intra.newLine)}</td></tr>`
      );
    }
  }
  return `<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <title>Diff</title>
  <style>
    body { font-family: Arial, sans-serif; margin: 1rem; }
    table { width: 100%; border-collapse: collapse; }
    td { border: 1px solid #ddd; padding: 0.4rem; vertical-align: top; white-space: pre-wrap; font-family: Consolas, monospace; }
    .add td { background: #e8ffe8; }
    .del td { background: #ffe8e8; }
    .mod td { background: #fff7db; }
  </style>
</head>
<body>
  <h1>Diff</h1>
  <table>${rows.join("\n")}</table>
</body>
</html>
`;
}

function diffTwoFiles(leftPath, rightPath, cfg) {
  if (!fs.existsSync(leftPath) || !fs.existsSync(rightPath)) {
    throw new Error("Missing input files");
  }
  const left = readLines(leftPath, cfg);
  const right = readLines(rightPath, cfg);
  if (left.binary || right.binary) {
    return {
      warning: "Binary file detected; diff skipped",
      output: "",
      stats: { totalOriginal: 0, totalModified: 0, added: 0, deleted: 0, modified: 0, similarityPct: 0 },
      ops: []
    };
  }
  const rawOps = lcsDiff(left.norm, right.norm);
  const ops = pairModifications(rawOps);
  const stats = diffStats(ops, left.lines.length, right.lines.length);

  let rendered = "";
  if (cfg.format === "unified") rendered = renderUnified(ops, left.lines, right.lines, cfg.context);
  else if (cfg.format === "side-by-side") rendered = renderSideBySide(ops, left.lines, right.lines);
  else if (cfg.format === "inline") rendered = renderInline(ops, left.lines, right.lines);
  else rendered = renderHtml(ops, left.lines, right.lines);

  return { warning: null, output: rendered, stats, ops };
}

function walkFiles(root) {
  const map = new Map();
  const stack = [root];
  while (stack.length) {
    const cur = stack.pop();
    for (const entry of fs.readdirSync(cur, { withFileTypes: true })) {
      const full = path.join(cur, entry.name);
      if (entry.isDirectory()) stack.push(full);
      else if (entry.isFile()) map.set(path.relative(root, full), full);
    }
  }
  return map;
}

function diffDirectories(dirA, dirB, cfg) {
  const filesA = walkFiles(dirA);
  const filesB = walkFiles(dirB);
  const allRel = new Set([...filesA.keys(), ...filesB.keys()]);
  const summary = { added: [], deleted: [], modified: [], identical: [] };

  for (const rel of [...allRel].sort()) {
    const a = filesA.get(rel);
    const b = filesB.get(rel);
    if (!a) {
      summary.added.push(rel);
      continue;
    }
    if (!b) {
      summary.deleted.push(rel);
      continue;
    }
    const res = diffTwoFiles(a, b, cfg);
    if (res.warning) {
      summary.modified.push(rel);
      continue;
    }
    const changed = res.ops.some((op) => op.type !== "equal");
    if (changed) summary.modified.push(rel);
    else summary.identical.push(rel);
  }

  const lines = [];
  lines.push("Directory diff summary");
  lines.push(`Added: ${summary.added.length}`);
  for (const p of summary.added) lines.push(colorize(`  + ${p}`, "green"));
  lines.push(`Deleted: ${summary.deleted.length}`);
  for (const p of summary.deleted) lines.push(colorize(`  - ${p}`, "red"));
  lines.push(`Modified: ${summary.modified.length}`);
  for (const p of summary.modified) lines.push(colorize(`  ~ ${p}`, "yellow"));
  lines.push(`Identical: ${summary.identical.length}`);
  for (const p of summary.identical) lines.push(`    ${p}`);
  return { summary, output: `${lines.join("\n")}\n` };
}

function createSamples() {
  const left = path.resolve("sample_original.txt");
  const right = path.resolve("sample_modified.txt");
  const a = [
    "Project Delta Status Report",
    "The team completed phase one on Monday.",
    "We tested the API and database integration.",
    "Performance baseline is 220 requests per second.",
    "Risks include deployment timing and data migration.",
    "Action: finalize rollback plan."
  ].join("\n");
  const b = [
    "Project Delta Status Report",
    "The team completed phase one on Tuesday.",
    "We tested API integration and caching layer.",
    "Performance baseline is 260 requests per second.",
    "Action: finalize rollback plan and run rehearsal.",
    "New note: monitor latency in production."
  ].join("\n");
  fs.writeFileSync(left, `${a}\n`, "utf8");
  fs.writeFileSync(right, `${b}\n`, "utf8");
  return { left, right };
}

function writeOutputMaybe(content, outPath) {
  if (!outPath) return;
  fs.writeFileSync(outPath, content, "utf8");
}

function main() {
  try {
    const cfg = parseArgs(process.argv.slice(2));
    let left;
    let right;
    let sampleMode = false;
    if (cfg.inputs.length >= 2) {
      [left, right] = cfg.inputs;
    } else {
      const s = createSamples();
      left = s.left;
      right = s.right;
      sampleMode = true;
    }

    if (cfg.recursive) {
      if (!fs.existsSync(left) || !fs.statSync(left).isDirectory()) throw new Error("First input must be a directory for --recursive");
      if (!fs.existsSync(right) || !fs.statSync(right).isDirectory()) throw new Error("Second input must be a directory for --recursive");
      const dirRes = diffDirectories(left, right, cfg);
      process.stdout.write(dirRes.output);
      writeOutputMaybe(dirRes.output, cfg.output);
      return;
    }

    if (!fs.existsSync(left) || !fs.existsSync(right)) throw new Error("Input files not found");
    if (fs.statSync(left).isDirectory() || fs.statSync(right).isDirectory()) {
      throw new Error("Use --recursive to compare directories");
    }

    const results = [];
    if (sampleMode) {
      for (const f of ["unified", "side-by-side", "inline", "html"]) {
        const one = { ...cfg, format: f };
        const res = diffTwoFiles(left, right, one);
        const block = `\n===== FORMAT: ${f} =====\n${res.output}`;
        process.stdout.write(block);
        results.push({ format: f, warning: res.warning, stats: res.stats, output: res.output });
      }
      writeOutputMaybe(results.map((r) => `===== FORMAT: ${r.format} =====\n${r.output}`).join("\n"), cfg.output);
    } else {
      const res = diffTwoFiles(left, right, cfg);
      if (res.warning) process.stdout.write(`Warning: ${res.warning}\n`);
      process.stdout.write(res.output);
      process.stdout.write(
        `Stats: original=${res.stats.totalOriginal}, modified=${res.stats.totalModified}, +${res.stats.added}, -${res.stats.deleted}, ~${res.stats.modified}, similarity=${res.stats.similarityPct.toFixed(
          2
        )}%\n`
      );
      writeOutputMaybe(res.output, cfg.output);
      results.push({ format: cfg.format, warning: res.warning, stats: res.stats, output: res.output });
    }

    const report = {
      generatedAt: new Date().toISOString(),
      original: left,
      modified: right,
      recursive: false,
      formats: results.map((r) => ({ format: r.format, warning: r.warning, stats: r.stats }))
    };
    fs.writeFileSync("diff_report.json", JSON.stringify(report, null, 2), "utf8");
  } catch (error) {
    process.stderr.write(`Error: ${error.message}\n`);
    process.exit(1);
  }
}

main();