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();