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

Time Series Trend Detector (javascript, written by Claude Code)

envgap__claude-code__javascript-t1-9

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 #9 · 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: 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

3 files, exactly as written, before any repair.

detector.js
#!/usr/bin/env node
/**
 * Time Series Trend Detector - JavaScript Trial 1
 * Uses simple-statistics + csv-parse + dayjs for time series analysis.
 */

const fs = require("fs");
const path = require("path");
const { parse } = require("csv-parse/sync");
const ss = require("simple-statistics");
const dayjs = require("dayjs");
const customParseFormat = require("dayjs/plugin/customParseFormat");

dayjs.extend(customParseFormat);

// --- Sample Data Generation ---

function generateSampleData(outputPath) {
  const nPoints = 730;
  const startDate = dayjs("2022-01-01");
  const dropIndices = new Set([50, 51, 200, 201, 202, 450]);
  const anomalyIndices = new Set([100, 250, 400, 550, 680]);

  // Simple seeded random (LCG)
  let seed = 42;
  function random() {
    seed = (seed * 1664525 + 1013904223) & 0xffffffff;
    return (seed >>> 0) / 0xffffffff;
  }
  function randomGaussian() {
    const u1 = random();
    const u2 = random();
    return Math.sqrt(-2 * Math.log(u1 || 0.001)) * Math.cos(2 * Math.PI * u2);
  }

  let rows = ["timestamp,value"];
  for (let i = 0; i < nPoints; i++) {
    if (dropIndices.has(i)) continue;

    const trend = 10 + (40 * i) / (nPoints - 1);
    const seasonal = 15 * Math.sin((2 * Math.PI * i) / 365.25);
    const weekly = 5 * Math.sin((2 * Math.PI * i) / 7);
    const noise = randomGaussian() * 3;
    let value = trend + seasonal + weekly + noise;

    if (anomalyIndices.has(i)) {
      value += (random() > 0.5 ? 1 : -1) * (30 + random() * 20);
    }

    const date = startDate.add(i, "day").format("YYYY-MM-DD");
    rows.push(`${date},${value.toFixed(4)}`);
  }

  fs.writeFileSync(outputPath, rows.join("\n") + "\n");
  console.log(`Generated sample data with ${rows.length - 1} points -> ${outputPath}`);
  return outputPath;
}

// --- Data Loading ---

function loadData(filepath) {
  const content = fs.readFileSync(filepath, "utf-8");
  const records = parse(content, { columns: true, skip_empty_lines: true, trim: true });

  if (records.length === 0) throw new Error("No data found in CSV");

  const columns = Object.keys(records[0]);
  let tsCol = null;
  let valCol = null;

  for (const col of columns) {
    const lower = col.toLowerCase().trim();
    if (["timestamp", "date", "datetime", "time", "ds"].includes(lower)) tsCol = col;
    else if (["value", "val", "y", "count", "amount", "price", "metric"].includes(lower)) valCol = col;
  }
  if (!tsCol) tsCol = columns[0];
  if (!valCol) valCol = columns[1];

  const data = records
    .map((r) => {
      const ts = dayjs(r[tsCol]);
      const val = parseFloat(r[valCol]);
      if (!ts.isValid() || isNaN(val)) return null;
      return { timestamp: ts, value: val };
    })
    .filter(Boolean)
    .sort((a, b) => a.timestamp.valueOf() - b.timestamp.valueOf());

  return data;
}

// --- Handle Missing Timestamps ---

function handleMissingTimestamps(data) {
  if (data.length < 2) return data;

  const diffs = [];
  for (let i = 1; i < data.length; i++) {
    diffs.push(data[i].timestamp.diff(data[i - 1].timestamp, "day"));
  }
  diffs.sort((a, b) => a - b);
  const medianDiff = diffs[Math.floor(diffs.length / 2)] || 1;

  const valueMap = new Map();
  for (const p of data) {
    valueMap.set(p.timestamp.format("YYYY-MM-DD"), p.value);
  }

  const filled = [];
  let current = data[0].timestamp;
  const end = data[data.length - 1].timestamp;
  let filledCount = 0;

  while (current.isBefore(end) || current.isSame(end)) {
    const key = current.format("YYYY-MM-DD");
    if (valueMap.has(key)) {
      filled.push({ timestamp: current, value: valueMap.get(key) });
    } else {
      // Find nearest before and after
      let prev = null;
      let next = null;
      for (const p of data) {
        if (p.timestamp.isBefore(current) || p.timestamp.isSame(current)) prev = p;
        if (p.timestamp.isAfter(current) && !next) next = p;
      }
      if (prev && next) {
        const totalDays = next.timestamp.diff(prev.timestamp, "day");
        const elapsed = current.diff(prev.timestamp, "day");
        const ratio = totalDays > 0 ? elapsed / totalDays : 0;
        const interpolated = prev.value + ratio * (next.value - prev.value);
        filled.push({ timestamp: current, value: interpolated });
        filledCount++;
      }
    }
    current = current.add(medianDiff, "day");
  }

  if (filledCount > 0) {
    console.log(`Filled ${filledCount} missing timestamps via interpolation.`);
  }
  return filled;
}

// --- Summary Statistics ---

function computeSummaryStats(data) {
  const values = data.map((d) => d.value);
  return {
    count: values.length,
    mean: ss.mean(values),
    std: ss.standardDeviation(values),
    min: ss.min(values),
    max: ss.max(values),
    median: ss.median(values),
    q25: ss.quantile(values, 0.25),
    q75: ss.quantile(values, 0.75),
    skewness: ss.sampleSkewness(values),
    kurtosis: computeKurtosis(values),
    startDate: data[0].timestamp.format("YYYY-MM-DD"),
    endDate: data[data.length - 1].timestamp.format("YYYY-MM-DD"),
    durationDays: data[data.length - 1].timestamp.diff(data[0].timestamp, "day"),
  };
}

function computeKurtosis(values) {
  const n = values.length;
  const mean = ss.mean(values);
  const std = ss.standardDeviation(values);
  if (std === 0) return 0;
  let sum4 = 0;
  for (const v of values) {
    sum4 += Math.pow((v - mean) / std, 4);
  }
  return sum4 / n - 3;
}

// --- Trend Detection ---

function detectTrend(data) {
  const n = data.length;
  const points = data.map((d, i) => [i, d.value]);

  // Linear regression
  const linReg = ss.linearRegression(points);
  const linLine = ss.linearRegressionLine(linReg);
  const rSquaredLin = ss.rSquared(points, linLine);

  // Polynomial degree 2
  const poly2Coeffs = polyFit(points, 2);
  const rSquaredPoly2 = polyRSquared(points, poly2Coeffs);

  // Polynomial degree 3
  const poly3Coeffs = polyFit(points, 3);
  const rSquaredPoly3 = polyRSquared(points, poly3Coeffs);

  let bestDegree = 1;
  let bestR2 = rSquaredLin;
  if (rSquaredPoly2 - rSquaredLin > 0.02) {
    bestDegree = 2;
    bestR2 = rSquaredPoly2;
  }
  if (rSquaredPoly3 - bestR2 > 0.02) {
    bestDegree = 3;
    bestR2 = rSquaredPoly3;
  }

  let direction = "flat";
  if (Math.abs(linReg.m) > 1e-6) {
    direction = linReg.m > 0 ? "upward" : "downward";
  }

  return {
    linearSlope: linReg.m,
    linearIntercept: linReg.b,
    linearRSquared: rSquaredLin,
    poly2RSquared: rSquaredPoly2,
    poly3RSquared: rSquaredPoly3,
    bestPolynomialDegree: bestDegree,
    bestRSquared: bestR2,
    direction: direction,
  };
}

function polyFit(points, degree) {
  // Least squares polynomial fit
  const n = points.length;
  const size = degree + 1;
  const X = [];
  const Y = [];

  for (const [x, y] of points) {
    const row = [];
    for (let d = 0; d <= degree; d++) {
      row.push(Math.pow(x, d));
    }
    X.push(row);
    Y.push(y);
  }

  // Normal equation: (X^T X) c = X^T Y
  const XtX = Array.from({ length: size }, () => new Array(size).fill(0));
  const XtY = new Array(size).fill(0);

  for (let i = 0; i < n; i++) {
    for (let j = 0; j < size; j++) {
      XtY[j] += X[i][j] * Y[i];
      for (let k = 0; k < size; k++) {
        XtX[j][k] += X[i][j] * X[i][k];
      }
    }
  }

  // Gauss elimination
  const aug = XtX.map((row, i) => [...row, XtY[i]]);
  for (let col = 0; col < size; col++) {
    let maxRow = col;
    for (let row = col + 1; row < size; row++) {
      if (Math.abs(aug[row][col]) > Math.abs(aug[maxRow][col])) maxRow = row;
    }
    [aug[col], aug[maxRow]] = [aug[maxRow], aug[col]];

    if (Math.abs(aug[col][col]) < 1e-12) continue;

    for (let row = col + 1; row < size; row++) {
      const factor = aug[row][col] / aug[col][col];
      for (let k = col; k <= size; k++) {
        aug[row][k] -= factor * aug[col][k];
      }
    }
  }

  const coeffs = new Array(size).fill(0);
  for (let i = size - 1; i >= 0; i--) {
    coeffs[i] = aug[i][size];
    for (let j = i + 1; j < size; j++) {
      coeffs[i] -= aug[i][j] * coeffs[j];
    }
    coeffs[i] /= aug[i][i] || 1;
  }

  return coeffs;
}

function polyEval(coeffs, x) {
  let val = 0;
  for (let i = 0; i < coeffs.length; i++) {
    val += coeffs[i] * Math.pow(x, i);
  }
  return val;
}

function polyRSquared(points, coeffs) {
  const ys = points.map((p) => p[1]);
  const yMean = ss.mean(ys);
  let ssTot = 0;
  let ssRes = 0;
  for (const [x, y] of points) {
    const predicted = polyEval(coeffs, x);
    ssRes += (y - predicted) ** 2;
    ssTot += (y - yMean) ** 2;
  }
  return ssTot > 0 ? 1 - ssRes / ssTot : 0;
}

// --- Seasonal Decomposition ---

function decomposeSeasons(data, period) {
  const n = data.length;
  const values = data.map((d) => d.value);

  // Trend via centered moving average
  const trend = new Array(n).fill(NaN);
  const halfP = Math.floor(period / 2);
  for (let i = halfP; i < n - halfP; i++) {
    let sum = 0;
    let count = 0;
    for (let j = i - halfP; j <= i + halfP; j++) {
      sum += values[j];
      count++;
    }
    trend[i] = sum / count;
  }

  // Detrended
  const detrended = values.map((v, i) => (isNaN(trend[i]) ? 0 : v - trend[i]));

  // Average seasonal pattern
  const seasonalPattern = new Array(period).fill(0);
  const seasonalCount = new Array(period).fill(0);
  for (let i = 0; i < n; i++) {
    if (!isNaN(trend[i])) {
      seasonalPattern[i % period] += detrended[i];
      seasonalCount[i % period]++;
    }
  }
  for (let i = 0; i < period; i++) {
    seasonalPattern[i] = seasonalCount[i] > 0 ? seasonalPattern[i] / seasonalCount[i] : 0;
  }

  // Build components
  const seasonal = values.map((_, i) => seasonalPattern[i % period]);
  const residual = values.map((v, i) =>
    isNaN(trend[i]) ? 0 : v - trend[i] - seasonal[i]
  );

  // Seasonal strength
  const validResid = residual.filter((_, i) => !isNaN(trend[i]));
  const validSR = seasonal
    .map((s, i) => s + residual[i])
    .filter((_, i) => !isNaN(trend[i]));
  const varResid = ss.variance(validResid);
  const varSR = ss.variance(validSR);
  const strength = varSR > 0 ? Math.max(0, Math.min(1, 1 - varResid / varSR)) : 0;

  return {
    period,
    seasonalStrength: strength,
    seasonalAmplitude: Math.max(...seasonalPattern) - Math.min(...seasonalPattern),
    residualStd: ss.standardDeviation(validResid),
    trendComponentMean: ss.mean(trend.filter((t) => !isNaN(t))),
  };
}

// --- Anomaly Detection ---

function detectAnomaliesZScore(data, threshold = 3.0) {
  const values = data.map((d) => d.value);
  const mean = ss.mean(values);
  const std = ss.standardDeviation(values);
  if (std === 0) return [];

  const anomalies = [];
  for (let i = 0; i < values.length; i++) {
    const z = Math.abs((values[i] - mean) / std);
    if (z > threshold) {
      anomalies.push({
        index: i,
        timestamp: data[i].timestamp.format("YYYY-MM-DD"),
        value: values[i],
        zScore: z,
        method: "z-score",
      });
    }
  }
  return anomalies;
}

function detectAnomaliesIQR(data, multiplier = 1.5) {
  const values = data.map((d) => d.value);
  const q1 = ss.quantile(values, 0.25);
  const q3 = ss.quantile(values, 0.75);
  const iqr = q3 - q1;
  const lower = q1 - multiplier * iqr;
  const upper = q3 + multiplier * iqr;

  const anomalies = [];
  for (let i = 0; i < values.length; i++) {
    if (values[i] < lower || values[i] > upper) {
      anomalies.push({
        index: i,
        timestamp: data[i].timestamp.format("YYYY-MM-DD"),
        value: values[i],
        lowerBound: lower,
        upperBound: upper,
        method: "IQR",
      });
    }
  }
  return anomalies;
}

// --- Moving Averages ---

function computeMovingAverages(data, windows = [7, 14, 30, 90]) {
  const values = data.map((d) => d.value);
  const results = {};

  for (const w of windows) {
    if (w >= data.length) continue;

    // SMA
    const sma = new Array(values.length);
    for (let i = 0; i < values.length; i++) {
      const start = Math.max(0, i - w + 1);
      let sum = 0;
      for (let j = start; j <= i; j++) sum += values[j];
      sma[i] = sum / (i - start + 1);
    }
    results[`SMA_${w}`] = sma;

    // EMA
    const ema = new Array(values.length);
    const alpha = 2 / (w + 1);
    ema[0] = values[0];
    for (let i = 1; i < values.length; i++) {
      ema[i] = alpha * values[i] + (1 - alpha) * ema[i - 1];
    }
    results[`EMA_${w}`] = ema;
  }

  return results;
}

// --- Console Report ---

function printConsoleReport(stats, trend, seasonal, zScoreAnomalies, iqrAnomalies) {
  console.log("\n" + "=".repeat(70));
  console.log("       TIME SERIES TREND DETECTION REPORT");
  console.log("=".repeat(70));

  console.log("\n--- Summary Statistics ---");
  console.log(`  Data Points:     ${stats.count}`);
  console.log(`  Date Range:      ${stats.startDate} to ${stats.endDate}`);
  console.log(`  Duration:        ${stats.durationDays} days`);
  console.log(`  Mean:            ${stats.mean.toFixed(4)}`);
  console.log(`  Std Dev:         ${stats.std.toFixed(4)}`);
  console.log(`  Min:             ${stats.min.toFixed(4)}`);
  console.log(`  Max:             ${stats.max.toFixed(4)}`);
  console.log(`  Median:          ${stats.median.toFixed(4)}`);
  console.log(`  Skewness:        ${stats.skewness.toFixed(4)}`);
  console.log(`  Kurtosis:        ${stats.kurtosis.toFixed(4)}`);

  console.log("\n--- Trend Analysis ---");
  console.log(`  Direction:       ${trend.direction}`);
  console.log(`  Linear Slope:    ${trend.linearSlope.toFixed(6)} per time step`);
  console.log(`  Linear R^2:      ${trend.linearRSquared.toFixed(4)}`);
  console.log(`  Poly(2) R^2:     ${trend.poly2RSquared.toFixed(4)}`);
  console.log(`  Poly(3) R^2:     ${trend.poly3RSquared.toFixed(4)}`);
  console.log(`  Best Fit Degree: ${trend.bestPolynomialDegree}`);

  console.log("\n--- Seasonal Decomposition ---");
  console.log(`  Period:          ${seasonal.period}`);
  console.log(`  Strength:        ${seasonal.seasonalStrength.toFixed(4)}`);
  console.log(`  Amplitude:       ${seasonal.seasonalAmplitude.toFixed(4)}`);
  console.log(`  Residual Std:    ${seasonal.residualStd.toFixed(4)}`);

  console.log("\n--- Anomaly Detection ---");
  console.log(`  Z-score anomalies: ${zScoreAnomalies.length}`);
  for (const a of zScoreAnomalies.slice(0, 5)) {
    console.log(`    ${a.timestamp}  value=${a.value.toFixed(2)}  z=${a.zScore.toFixed(2)}`);
  }
  if (zScoreAnomalies.length > 5) {
    console.log(`    ... and ${zScoreAnomalies.length - 5} more`);
  }

  console.log(`  IQR anomalies:     ${iqrAnomalies.length}`);
  for (const a of iqrAnomalies.slice(0, 5)) {
    console.log(`    ${a.timestamp}  value=${a.value.toFixed(2)}`);
  }
  if (iqrAnomalies.length > 5) {
    console.log(`    ... and ${iqrAnomalies.length - 5} more`);
  }

  console.log("\n" + "=".repeat(70));
}

// --- Main ---

function main() {
  const args = process.argv.slice(2);
  let inputPath = null;
  let outputDir = ".";
  let zScoreThreshold = 3.0;
  let iqrMultiplier = 1.5;
  let seasonalPeriod = -1;

  for (let i = 0; i < args.length; i++) {
    switch (args[i]) {
      case "--input":
      case "-i":
        inputPath = args[++i];
        break;
      case "--output":
      case "-o":
        outputDir = args[++i];
        break;
      case "--zscore-threshold":
        zScoreThreshold = parseFloat(args[++i]);
        break;
      case "--iqr-multiplier":
        iqrMultiplier = parseFloat(args[++i]);
        break;
      case "--period":
      case "-p":
        seasonalPeriod = parseInt(args[++i]);
        break;
    }
  }

  if (!fs.existsSync(outputDir)) {
    fs.mkdirSync(outputDir, { recursive: true });
  }

  if (!inputPath) {
    console.log("No input file specified. Generating sample data...");
    inputPath = path.join(outputDir, "sample_timeseries.csv");
    generateSampleData(inputPath);
  }

  console.log(`Loading data from ${inputPath}...`);
  let data = loadData(inputPath);
  console.log(`Loaded ${data.length} records.`);

  data = handleMissingTimestamps(data);
  console.log(`After filling gaps: ${data.length} records.`);

  const stats = computeSummaryStats(data);
  const trend = detectTrend(data);

  if (seasonalPeriod < 0) {
    const n = data.length;
    if (n >= 730) seasonalPeriod = 365;
    else if (n >= 60) seasonalPeriod = 30;
    else if (n >= 14) seasonalPeriod = 7;
    else seasonalPeriod = Math.max(2, Math.floor(n / 3));
  }
  if (seasonalPeriod >= Math.floor(data.length / 2)) {
    seasonalPeriod = Math.max(2, Math.floor(data.length / 3));
  }

  const seasonal = decomposeSeasons(data, seasonalPeriod);
  const zScoreAnomalies = detectAnomaliesZScore(data, zScoreThreshold);
  const iqrAnomalies = detectAnomaliesIQR(data, iqrMultiplier);
  const movingAverages = computeMovingAverages(data);

  printConsoleReport(stats, trend, seasonal, zScoreAnomalies, iqrAnomalies);

  // Save JSON report
  const report = {
    generatedAt: new Date().toISOString(),
    summaryStatistics: stats,
    trendAnalysis: trend,
    seasonalDecomposition: seasonal,
    anomalies: {
      zScore: {
        count: zScoreAnomalies.length,
        threshold: zScoreThreshold,
        detections: zScoreAnomalies,
      },
      iqr: {
        count: iqrAnomalies.length,
        multiplier: iqrMultiplier,
        detections: iqrAnomalies,
      },
    },
  };

  const reportPath = path.join(outputDir, "trend_report.json");
  fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
  console.log(`Report saved to ${reportPath}`);
  console.log("\nAnalysis complete.");
}

main();
package.json
{
  "name": "time-series-trend-detector-t1",
  "version": "1.0.0",
  "description": "Time Series Trend Detector using simple-statistics, csv-parse, and dayjs",
  "main": "detector.js",
  "scripts": {
    "start": "node detector.js",
    "detect": "node detector.js"
  },
  "dependencies": {
    "simple-statistics": "7.8.3",
    "csv-parse": "5.5.3",
    "dayjs": "1.11.10"
  }
}
README.md
# Time Series Trend Detector - JavaScript Trial 1

Analyzes time series data to detect trends, seasonal patterns, and anomalies using simple-statistics, csv-parse, and dayjs.

## Dependencies

- simple-statistics 7.8.3
- csv-parse 5.5.3
- dayjs 1.11.10

## Usage

```bash
npm install
node detector.js

# With input file
node detector.js --input data.csv --output results/
```

## Output

- Console report with summary statistics, trend analysis, seasonal decomposition, and anomaly detection
- `trend_report.json` - Full analysis results in JSON format