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

Time Series Trend Detector (javascript, written by Codex)

envgap__codex__javascript-t1-9

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

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 #9 · 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: Time Series Trend Detector

Write a program that analyzes time series data to detect trends, seasonal patterns, and anomalies using statistical methods, and produces a visual summary report.

FUNCTIONAL REQUIREMENTS:
- Accept a CSV file path as a command-line argument with columns for timestamp and one or more numeric value columns
- Parse timestamps in multiple formats (ISO 8601, Unix epoch, and common date formats like MM/DD/YYYY, YYYY-MM-DD HH:MM:SS)
- Compute a moving average with a configurable window size via --window flag (default: 7 data points)
- Detect overall trend direction (increasing, decreasing, stable) using linear regression and report the slope and R-squared value
- Detect seasonality by computing autocorrelation at various lags and reporting the dominant period if one exists
- Identify anomalies: data points that deviate more than a configurable number of standard deviations from the moving average (--threshold flag, default: 2.0)
- Support multiple value columns: analyze each independently and report results for all
- Generate a summary report with: trend direction and strength, seasonal period (if any), count and list of anomalies with their timestamps and values, basic statistics (min, max, mean, variance)
- Save the report as a JSON file with --output flag (default: trend_report.json)
- Export the processed data (original values, moving average, anomaly flags) as a CSV file via --export flag
- If no input file is given, generate a sample time series dataset with 365 daily data points containing a linear trend, weekly seasonality, and injected anomalies, then analyze it
- Handle missing timestamps and gaps in the series by interpolating or flagging them

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": "time-series-trend-detector",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "time-series-trend-detector",
      "version": "1.0.0",
      "dependencies": {}
    }
  }
}
package.json
{
  "name": "time-series-trend-detector",
  "version": "1.0.0",
  "description": "Detect trends, seasonality, and anomalies in CSV time series data.",
  "type": "module",
  "main": "src/index.js",
  "scripts": {
    "start": "node src/index.js"
  },
  "engines": {
    "node": ">=20.0.0"
  },
  "dependencies": {}
}
README.md
# Time Series Trend Detector (JavaScript)

Analyzes CSV time series data for trend, seasonality, anomalies, and summary statistics.

## Requirements

- Ubuntu 22.04
- Node.js 20+ (LTS)

## Dependencies

- Direct: none
- Transitive: none

Pinned in `package.json` and `package-lock.json`.

## Run

With input:

```bash
node src/index.js /path/to/data.csv --window 7 --threshold 2.0 --output trend_report.json --export processed.csv
```

No input (generates 365-point sample and analyzes it):

```bash
node src/index.js
```

## Features

- Timestamp parsing: ISO 8601, Unix epoch (10/13 digits), `MM/DD/YYYY`, `YYYY-MM-DD HH:MM:SS`
- Moving average (`--window`)
- Trend by linear regression (slope, R-squared, direction)
- Seasonality via autocorrelation lag scan
- Anomaly detection using deviation from moving average (`--threshold` standard deviations)
- Multi-column independent analysis
- Gap handling through interpolation and gap reporting
- JSON report + processed CSV export (value, moving average, anomaly flag)
src/index.js
import fs from "fs";
import path from "path";

function parseArgs(argv) {
  const options = {
    window: "7",
    threshold: "2.0",
    output: "trend_report.json",
  };
  const positional = [];
  for (let i = 0; i < argv.length; i += 1) {
    const token = argv[i];
    if (token.startsWith("--")) {
      const key = token.slice(2);
      const next = argv[i + 1];
      if (next && !next.startsWith("--")) {
        options[key] = next;
        i += 1;
      } else {
        options[key] = true;
      }
    } else {
      positional.push(token);
    }
  }
  return { options, positional };
}

function parseCsvLine(line) {
  const out = [];
  let current = "";
  let inQuotes = false;
  for (let i = 0; i < line.length; i += 1) {
    const ch = line[i];
    if (ch === '"') {
      if (inQuotes && line[i + 1] === '"') {
        current += '"';
        i += 1;
      } else {
        inQuotes = !inQuotes;
      }
    } else if (ch === "," && !inQuotes) {
      out.push(current);
      current = "";
    } else {
      current += ch;
    }
  }
  out.push(current);
  return out;
}

function csvEscape(v) {
  const s = v == null ? "" : String(v);
  if (s.includes(",") || s.includes('"') || s.includes("\n")) return `"${s.replace(/"/g, "\"\"")}"`;
  return s;
}

function parseTimestamp(raw) {
  if (raw == null) return null;
  const s = String(raw).trim();
  if (!s) return null;

  if (/^\d{10}$/.test(s)) return Number.parseInt(s, 10) * 1000;
  if (/^\d{13}$/.test(s)) return Number.parseInt(s, 10);

  if (/^\d{1,2}\/\d{1,2}\/\d{4}$/.test(s)) {
    const [m, d, y] = s.split("/").map(Number);
    return Date.UTC(y, m - 1, d);
  }

  if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(s)) {
    const iso = s.replace(" ", "T") + "Z";
    const t = Date.parse(iso);
    return Number.isNaN(t) ? null : t;
  }

  const t = Date.parse(s);
  return Number.isNaN(t) ? null : t;
}

function mean(arr) {
  if (arr.length === 0) return null;
  return arr.reduce((a, b) => a + b, 0) / arr.length;
}

function variance(arr, m = null) {
  if (arr.length === 0) return null;
  const mu = m == null ? mean(arr) : m;
  return arr.reduce((s, x) => s + (x - mu) ** 2, 0) / arr.length;
}

function stddev(arr, m = null) {
  const v = variance(arr, m);
  return v == null ? null : Math.sqrt(v);
}

function linearRegression(values) {
  const points = [];
  for (let i = 0; i < values.length; i += 1) {
    if (values[i] != null && Number.isFinite(values[i])) points.push([i, values[i]]);
  }
  if (points.length < 2) return { slope: 0, intercept: 0, r2: 0 };

  const xs = points.map(([x]) => x);
  const ys = points.map(([, y]) => y);
  const mx = mean(xs);
  const my = mean(ys);
  let cov = 0;
  let vx = 0;
  for (let i = 0; i < points.length; i += 1) {
    cov += (xs[i] - mx) * (ys[i] - my);
    vx += (xs[i] - mx) ** 2;
  }
  const slope = vx === 0 ? 0 : cov / vx;
  const intercept = my - slope * mx;
  let ssRes = 0;
  let ssTot = 0;
  for (let i = 0; i < points.length; i += 1) {
    const pred = slope * xs[i] + intercept;
    ssRes += (ys[i] - pred) ** 2;
    ssTot += (ys[i] - my) ** 2;
  }
  const r2 = ssTot === 0 ? 0 : 1 - ssRes / ssTot;
  return { slope, intercept, r2 };
}

function movingAverage(values, window) {
  const out = Array(values.length).fill(null);
  for (let i = 0; i < values.length; i += 1) {
    const start = Math.max(0, i - window + 1);
    const slice = [];
    for (let j = start; j <= i; j += 1) if (values[j] != null) slice.push(values[j]);
    out[i] = slice.length ? mean(slice) : null;
  }
  return out;
}

function autocorrelation(values, lag) {
  const pairs = [];
  for (let i = lag; i < values.length; i += 1) {
    if (values[i] == null || values[i - lag] == null) continue;
    pairs.push([values[i], values[i - lag]]);
  }
  if (pairs.length < 3) return null;
  const xs = pairs.map(([x]) => x);
  const ys = pairs.map(([, y]) => y);
  const mx = mean(xs);
  const my = mean(ys);
  let num = 0;
  let dx = 0;
  let dy = 0;
  for (let i = 0; i < pairs.length; i += 1) {
    num += (xs[i] - mx) * (ys[i] - my);
    dx += (xs[i] - mx) ** 2;
    dy += (ys[i] - my) ** 2;
  }
  const den = Math.sqrt(dx * dy);
  return den === 0 ? 0 : num / den;
}

function detectSeasonality(values) {
  const maxLag = Math.min(60, Math.floor(values.length / 2));
  let bestLag = null;
  let bestCorr = 0;
  for (let lag = 2; lag <= maxLag; lag += 1) {
    const corr = autocorrelation(values, lag);
    if (corr == null) continue;
    if (Math.abs(corr) > Math.abs(bestCorr)) {
      bestCorr = corr;
      bestLag = lag;
    }
  }
  if (bestLag == null || Math.abs(bestCorr) < 0.3) return { period: null, autocorrelation: null };
  return { period: bestLag, autocorrelation: bestCorr };
}

function detectTrendDirection(slope, yStd) {
  const scale = yStd == null || yStd === 0 ? 1 : yStd;
  if (Math.abs(slope) < scale * 0.001) return "stable";
  return slope > 0 ? "increasing" : "decreasing";
}

function inferTimeStep(sortedTimestamps) {
  const diffs = [];
  for (let i = 1; i < sortedTimestamps.length; i += 1) {
    const d = sortedTimestamps[i] - sortedTimestamps[i - 1];
    if (d > 0) diffs.push(d);
  }
  if (diffs.length === 0) return 24 * 3600 * 1000;
  diffs.sort((a, b) => a - b);
  return diffs[Math.floor(diffs.length / 2)];
}

function interpolateGaps(series, valueColumns) {
  const sorted = [...series].sort((a, b) => a.timestampMs - b.timestampMs);
  const step = inferTimeStep(sorted.map((r) => r.timestampMs));
  const byTs = new Map(sorted.map((r) => [r.timestampMs, r]));
  const filled = [];
  const gapFlags = [];
  let t = sorted[0].timestampMs;
  const end = sorted[sorted.length - 1].timestampMs;
  while (t <= end) {
    if (byTs.has(t)) {
      filled.push({ ...byTs.get(t), gap_filled: false });
    } else {
      const row = {
        timestampMs: t,
        timestamp: new Date(t).toISOString(),
        gap_filled: true,
      };
      for (const col of valueColumns) row[col] = null;
      filled.push(row);
      gapFlags.push(t);
    }
    t += step;
  }

  for (const col of valueColumns) {
    for (let i = 0; i < filled.length; i += 1) {
      if (filled[i][col] != null) continue;
      let left = i - 1;
      while (left >= 0 && filled[left][col] == null) left -= 1;
      let right = i + 1;
      while (right < filled.length && filled[right][col] == null) right += 1;
      if (left >= 0 && right < filled.length) {
        const ratio = (filled[i].timestampMs - filled[left].timestampMs) / (filled[right].timestampMs - filled[left].timestampMs);
        filled[i][col] = filled[left][col] + (filled[right][col] - filled[left][col]) * ratio;
      }
    }
  }
  return { filled, step, gapFlags };
}

function parseInputCsv(filePath) {
  const text = fs.readFileSync(filePath, "utf8").replace(/^\uFEFF/, "");
  const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n").filter((l) => l.trim());
  if (lines.length === 0) return { rows: [], valueColumns: [] };
  const headers = parseCsvLine(lines[0]).map((h) => h.trim());
  const timestampColumn = headers[0];
  const valueColumns = headers.slice(1);
  const rows = [];
  for (const line of lines.slice(1)) {
    const parts = parseCsvLine(line);
    const ts = parseTimestamp(parts[0]);
    if (ts == null) continue;
    const row = {
      timestampMs: ts,
      timestamp: new Date(ts).toISOString(),
    };
    for (let i = 0; i < valueColumns.length; i += 1) {
      const raw = parts[i + 1];
      const num = raw == null || String(raw).trim() === "" ? null : Number(raw);
      row[valueColumns[i]] = Number.isFinite(num) ? num : null;
    }
    rows.push(row);
  }
  return { rows, valueColumns, timestampColumn };
}

function generateSample(filePath) {
  const start = Date.UTC(2025, 0, 1);
  const lines = ["timestamp,metric_a,metric_b"];
  for (let i = 0; i < 365; i += 1) {
    const ts = new Date(start + i * 24 * 3600 * 1000).toISOString().slice(0, 10);
    const weekly = 10 * Math.sin((2 * Math.PI * i) / 7);
    const trend = i * 0.18;
    let a = 50 + trend + weekly + (Math.sin(i) * 0.8);
    let b = 30 + i * 0.05 + 5 * Math.cos((2 * Math.PI * i) / 7);
    if (i === 45 || i === 123 || i === 251) a += 35;
    if (i === 200 || i === 300) b -= 20;
    lines.push(`${ts},${a.toFixed(3)},${b.toFixed(3)}`);
  }
  fs.writeFileSync(filePath, `${lines.join("\n")}\n`, "utf8");
}

function analyzeSeries(filledRows, col, window, threshold) {
  const values = filledRows.map((r) => r[col]);
  const ma = movingAverage(values, window);
  const validValues = values.filter((v) => v != null);
  const mu = mean(validValues);
  const varr = variance(validValues, mu);
  const sd = stddev(validValues, mu);
  const trend = linearRegression(values);
  const season = detectSeasonality(values);
  const direction = detectTrendDirection(trend.slope, sd);

  const anomalies = [];
  for (let i = 0; i < values.length; i += 1) {
    const v = values[i];
    if (v == null || ma[i] == null || sd == null || sd === 0) continue;
    const z = Math.abs(v - ma[i]) / sd;
    if (z > threshold) {
      anomalies.push({
        index: i,
        timestamp: filledRows[i].timestamp,
        value: v,
        moving_average: ma[i],
        z_from_moving_average: z,
      });
    }
  }

  return {
    column: col,
    stats: {
      min: validValues.length ? Math.min(...validValues) : null,
      max: validValues.length ? Math.max(...validValues) : null,
      mean: mu,
      variance: varr,
      stddev: sd,
    },
    trend: {
      direction,
      slope: trend.slope,
      r_squared: trend.r2,
    },
    seasonality: season,
    anomaly_count: anomalies.length,
    anomalies,
    moving_average: ma,
  };
}

function exportProcessedCsv(filePath, rows, analyses) {
  const valueCols = analyses.map((a) => a.column);
  const header = ["timestamp"];
  for (const c of valueCols) {
    header.push(c);
    header.push(`${c}_moving_average`);
    header.push(`${c}_is_anomaly`);
  }
  const lines = [header.join(",")];
  for (let i = 0; i < rows.length; i += 1) {
    const row = [rows[i].timestamp];
    for (const analysis of analyses) {
      const c = analysis.column;
      const value = rows[i][c];
      const ma = analysis.moving_average[i];
      const isAnomaly = analysis.anomalies.some((a) => a.index === i);
      row.push(csvEscape(value));
      row.push(csvEscape(ma));
      row.push(isAnomaly ? "1" : "0");
    }
    lines.push(row.join(","));
  }
  fs.writeFileSync(filePath, `${lines.join("\n")}\n`, "utf8");
}

function printSummary(analyses, gapFlags, outputPath, exportPath) {
  console.log("Time Series Trend Detector");
  console.log("==========================");
  console.log(`Columns analyzed: ${analyses.length}`);
  console.log(`Gap points filled/interpolated: ${gapFlags.length}`);
  console.log("");
  for (const a of analyses) {
    console.log(`Column: ${a.column}`);
    console.log(`  Trend      : ${a.trend.direction} (slope=${a.trend.slope.toFixed(6)}, R^2=${a.trend.r_squared.toFixed(4)})`);
    console.log(`  Seasonality: ${a.seasonality.period == null ? "none" : `period=${a.seasonality.period} lag (autocorr=${a.seasonality.autocorrelation.toFixed(4)})`}`);
    console.log(`  Anomalies  : ${a.anomaly_count}`);
    console.log(`  Stats      : min=${a.stats.min}, max=${a.stats.max}, mean=${a.stats.mean}, variance=${a.stats.variance}`);
    console.log("");
  }
  console.log(`JSON report saved : ${outputPath}`);
  if (exportPath) console.log(`Processed CSV saved: ${exportPath}`);
}

function main() {
  const { options, positional } = parseArgs(process.argv.slice(2));
  const window = Math.max(1, Number.parseInt(options.window, 10) || 7);
  const threshold = Number.parseFloat(options.threshold) || 2.0;
  const outputPath = path.resolve(options.output || "trend_report.json");
  const exportPath = options.export ? path.resolve(options.export) : null;

  let inputPath;
  if (positional.length === 0) {
    inputPath = path.resolve("sample_timeseries.csv");
    generateSample(inputPath);
    console.log(`No input provided. Generated sample dataset: ${inputPath}`);
  } else {
    inputPath = path.resolve(positional[0]);
    if (!fs.existsSync(inputPath)) {
      console.error(`Input file not found: ${inputPath}`);
      process.exit(1);
    }
  }

  const parsed = parseInputCsv(inputPath);
  if (!parsed.rows.length || !parsed.valueColumns.length) {
    console.error("No valid rows or value columns found.");
    process.exit(1);
  }

  const { filled, step, gapFlags } = interpolateGaps(parsed.rows, parsed.valueColumns);
  const analyses = parsed.valueColumns.map((col) => analyzeSeries(filled, col, window, threshold));

  const report = {
    metadata: {
      input_file: inputPath,
      generated_at: new Date().toISOString(),
      window,
      threshold,
      inferred_step_millis: step,
      interpolated_gap_points: gapFlags.map((t) => new Date(t).toISOString()),
    },
    timestamp_column: parsed.timestampColumn || "timestamp",
    value_columns: parsed.valueColumns,
    analyses: analyses.map((a) => ({
      column: a.column,
      stats: a.stats,
      trend: a.trend,
      seasonality: a.seasonality,
      anomaly_count: a.anomaly_count,
      anomalies: a.anomalies.map((x) => ({
        timestamp: x.timestamp,
        value: x.value,
        moving_average: x.moving_average,
        z_from_moving_average: x.z_from_moving_average,
      })),
    })),
  };

  fs.writeFileSync(outputPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
  if (exportPath) exportProcessedCsv(exportPath, filled, analyses);
  printSummary(analyses, gapFlags, outputPath, exportPath);
}

main();