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