Zero-Dependency KPI Forecasting for Autonomous Systems: Applying the Digital Twin Principle to Operational Metrics with Pure JavaScript Linear Regression
SKILL: Predictive KPI Forecasting from Time-Series Snapshots
name: kpi-forecasting-digital-twin version: 1.0.0 author: aiindigo-simulation description: Apply linear regression to hourly operational KPI snapshots to forecast milestone dates, detect growth plateaus, and predict resource depletion — zero external dependencies, pure JavaScript dependencies:
- node.js >= 18 (no npm packages required) inputs:
- chronicle.jsonl (append-only JSONL file, one snapshot per line) outputs:
- forecasts.json (projections + milestone dates)
- anomalies.json (plateau detection, depletion alerts)
Chronicle Format
Each line in chronicle.jsonl is a JSON snapshot of operational state:
{"ts": 1711497600000, "tools_count": 6531, "enrichment_pct": 42.3, "blogs_published": 183, "content_queue": 618, "cache_hit_rate": 7.7, "disk_gb_used": 12.4}
{"ts": 1711501200000, "tools_count": 6534, "enrichment_pct": 42.5, "blogs_published": 184, "content_queue": 615, "disk_gb_used": 12.5}Steps
Step 1 — Load and Parse Chronicle
const fs = require('fs');
const path = require('path');
function loadChronicle(chroniclePath = 'data/chronicle.jsonl') {
if (!fs.existsSync(chroniclePath)) {
throw new Error(`Chronicle not found: ${chroniclePath}`);
}
const lines = fs.readFileSync(chroniclePath, 'utf8')
.split('\n')
.filter(l => l.trim());
const snapshots = [];
for (const line of lines) {
try {
const snap = JSON.parse(line);
if (snap.ts && typeof snap.ts === 'number') {
snapshots.push(snap);
}
} catch (e) {
// Skip malformed lines
}
}
snapshots.sort((a, b) => a.ts - b.ts);
console.log(`Loaded <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>s</mi><mi>n</mi><mi>a</mi><mi>p</mi><mi>s</mi><mi>h</mi><mi>o</mi><mi>t</mi><mi>s</mi><mi mathvariant="normal">.</mi><mi>l</mi><mi>e</mi><mi>n</mi><mi>g</mi><mi>t</mi><mi>h</mi></mrow><mi>s</mi><mi>n</mi><mi>a</mi><mi>p</mi><mi>s</mi><mi>h</mi><mi>o</mi><mi>t</mi><mi>s</mi><mi>s</mi><mi>p</mi><mi>a</mi><mi>n</mi><mi>n</mi><mi>i</mi><mi>n</mi><mi>g</mi></mrow><annotation encoding="application/x-tex">{snapshots.length} snapshots spanning</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathnormal">s</span><span class="mord mathnormal">na</span><span class="mord mathnormal">p</span><span class="mord mathnormal">s</span><span class="mord mathnormal">h</span><span class="mord mathnormal">o</span><span class="mord mathnormal">t</span><span class="mord mathnormal">s</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.0197em;">l</span><span class="mord mathnormal">e</span><span class="mord mathnormal">n</span><span class="mord mathnormal" style="margin-right:0.0359em;">g</span><span class="mord mathnormal">t</span><span class="mord mathnormal">h</span></span><span class="mord mathnormal">s</span><span class="mord mathnormal">na</span><span class="mord mathnormal">p</span><span class="mord mathnormal">s</span><span class="mord mathnormal">h</span><span class="mord mathnormal">o</span><span class="mord mathnormal">t</span><span class="mord mathnormal">ss</span><span class="mord mathnormal">p</span><span class="mord mathnormal">annin</span><span class="mord mathnormal" style="margin-right:0.0359em;">g</span></span></span></span>{
Math.round((snapshots[snapshots.length-1].ts - snapshots[0].ts) / 86400000)
} days`);
return snapshots;
}Step 2 — Group Snapshots by Day
Aggregate hourly snapshots into daily summaries for cleaner trend lines.
function groupByDay(snapshots) {
const days = {};
for (const snap of snapshots) {
const day = new Date(snap.ts).toISOString().split('T')[0];
if (!days[day]) days[day] = [];
days[day].push(snap);
}
// For each day, use the last snapshot of the day as the daily value
const dailySeries = Object.entries(days)
.sort(([a], [b]) => a.localeCompare(b))
.map(([date, snaps]) => {
const last = snaps[snaps.length - 1];
return { date, ts: last.ts, ...last };
});
console.log(`Grouped into ${dailySeries.length} daily data points`);
return dailySeries;
}Step 3 — Linear Regression
Pure JavaScript linear regression — no dependencies.
function linearRegression(points) {
// points: [{x: number, y: number}, ...]
const n = points.length;
if (n < 2) return null;
const sumX = points.reduce((s, p) => s + p.x, 0);
const sumY = points.reduce((s, p) => s + p.y, 0);
const sumXY = points.reduce((s, p) => s + p.x * p.y, 0);
const sumX2 = points.reduce((s, p) => s + p.x * p.x, 0);
const denom = n * sumX2 - sumX * sumX;
if (denom === 0) return null;
const slope = (n * sumXY - sumX * sumY) / denom;
const intercept = (sumY - slope * sumX) / n;
// R² — coefficient of determination
const yMean = sumY / n;
const ssTot = points.reduce((s, p) => s + (p.y - yMean) ** 2, 0);
const ssRes = points.reduce((s, p) => s + (p.y - (slope * p.x + intercept)) ** 2, 0);
const r2 = ssTot > 0 ? 1 - ssRes / ssTot : 0;
return { slope, intercept, r2 };
}Step 4 — Project Forward and Find Milestones
Forecast each metric 7, 14, 30, and 90 days ahead. Find when it hits key milestones.
function projectMetric(series, metric, milestones = []) {
const validPoints = series
.filter(s => s[metric] != null && !isNaN(s[metric]))
.map((s, i) => ({ x: i, y: s[metric], ts: s.ts }));
if (validPoints.length < 3) return null;
const reg = linearRegression(validPoints);
if (!reg) return null;
const lastIdx = validPoints.length - 1;
const lastTs = validPoints[lastIdx].ts;
const dayMs = 86400000;
// Project forward
const horizons = [7, 14, 30, 90];
const projections = {};
for (const days of horizons) {
const futureIdx = lastIdx + days;
projections[`in_${days}d`] = {
value: Math.max(0, reg.slope * futureIdx + reg.intercept),
date: new Date(lastTs + days * dayMs).toISOString().split('T')[0]
};
}
// Milestone dates
const milestoneDates = {};
for (const target of milestones) {
if (reg.slope <= 0) {
milestoneDates[target] = null; // Not reachable with current trend
continue;
}
const targetIdx = (target - reg.intercept) / reg.slope;
const daysToTarget = Math.round(targetIdx - lastIdx);
if (daysToTarget < 0 || daysToTarget > 365) {
milestoneDates[target] = null;
} else {
milestoneDates[target] = new Date(lastTs + daysToTarget * dayMs)
.toISOString().split('T')[0];
}
}
return {
metric,
currentValue: validPoints[lastIdx].y,
slope: Math.round(reg.slope * 1000) / 1000,
slopePerDay: Math.round(reg.slope * 10) / 10,
r2: Math.round(reg.r2 * 1000) / 1000,
trend: reg.slope > 0.01 ? 'growing' : reg.slope < -0.01 ? 'declining' : 'plateau',
projections,
milestones: milestoneDates,
dataPoints: validPoints.length
};
}Step 5 — Detect Plateaus and Anomalies
A plateau is when recent growth rate drops to < 10% of historical average.
function detectAnomalies(series, metric) {
const values = series
.filter(s => s[metric] != null)
.map(s => s[metric]);
if (values.length < 7) return [];
const anomalies = [];
// Plateau detection: compare last 7 days vs previous 7 days
const recent = values.slice(-7);
const prior = values.slice(-14, -7);
if (prior.length >= 7) {
const recentGrowth = (recent[recent.length-1] - recent[0]) / (Math.abs(recent[0]) || 1);
const priorGrowth = (prior[prior.length-1] - prior[0]) / (Math.abs(prior[0]) || 1);
if (Math.abs(priorGrowth) > 0.05 && Math.abs(recentGrowth) < 0.01) {
anomalies.push({
type: 'plateau',
metric,
message: `<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>m</mi><mi>e</mi><mi>t</mi><mi>r</mi><mi>i</mi><mi>c</mi></mrow><mi>s</mi><mi>t</mi><mi>a</mi><mi>l</mi><mi>l</mi><mi>e</mi><mi>d</mi><mtext>—</mtext><mi>w</mi><mi>a</mi><mi>s</mi><mi>g</mi><mi>r</mi><mi>o</mi><mi>w</mi><mi>i</mi><mi>n</mi><mi>g</mi></mrow><annotation encoding="application/x-tex">{metric} stalled — was growing</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathnormal">m</span><span class="mord mathnormal">e</span><span class="mord mathnormal">t</span><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal">i</span><span class="mord mathnormal">c</span></span><span class="mord mathnormal">s</span><span class="mord mathnormal">t</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.0197em;">l</span><span class="mord mathnormal" style="margin-right:0.0197em;">l</span><span class="mord mathnormal">e</span><span class="mord mathnormal">d</span><span class="mord">—</span><span class="mord mathnormal" style="margin-right:0.0269em;">w</span><span class="mord mathnormal">a</span><span class="mord mathnormal">s</span><span class="mord mathnormal" style="margin-right:0.0359em;">g</span><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal">o</span><span class="mord mathnormal" style="margin-right:0.0269em;">w</span><span class="mord mathnormal">in</span><span class="mord mathnormal" style="margin-right:0.0359em;">g</span></span></span></span>{(priorGrowth*100).toFixed(1)}%/week, now flat`,
severity: 'warn'
});
}
}
// Sharp drop detection: last value > 20% below 7-day average
const recentAvg = recent.reduce((s, v) => s + v, 0) / recent.length;
const lastVal = values[values.length - 1];
if (lastVal < recentAvg * 0.8) {
anomalies.push({
type: 'sharp_drop',
metric,
message: `<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>m</mi><mi>e</mi><mi>t</mi><mi>r</mi><mi>i</mi><mi>c</mi></mrow><mi>d</mi><mi>r</mi><mi>o</mi><mi>p</mi><mi>p</mi><mi>e</mi><mi>d</mi><mi>t</mi><mi>o</mi></mrow><annotation encoding="application/x-tex">{metric} dropped to</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathnormal">m</span><span class="mord mathnormal">e</span><span class="mord mathnormal">t</span><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal">i</span><span class="mord mathnormal">c</span></span><span class="mord mathnormal">d</span><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal">o</span><span class="mord mathnormal">pp</span><span class="mord mathnormal">e</span><span class="mord mathnormal">d</span><span class="mord mathnormal">t</span><span class="mord mathnormal">o</span></span></span></span>{lastVal} (20%+ below 7-day avg ${recentAvg.toFixed(1)})`,
severity: 'alert'
});
}
return anomalies;
}Step 6 — Predict Resource Depletion
Alert when queues or capacity resources will run out within 48 hours.
function checkDepletion(series, resourceMetric, depletionValue = 0) {
const values = series
.filter(s => s[resourceMetric] != null)
.map((s, i) => ({ x: i, y: s[resourceMetric] }));
if (values.length < 5) return null;
const reg = linearRegression(values);
if (!reg || reg.slope >= 0) return null; // Not declining
// Days until depletion
const lastIdx = values.length - 1;
const currentVal = values[lastIdx].y;
const daysToDepletion = Math.round((depletionValue - currentVal) / reg.slope);
if (daysToDepletion <= 2) {
return {
type: 'depletion_warning',
resource: resourceMetric,
currentValue: currentVal,
daysToDepletion,
message: `⚠️ <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>r</mi><mi>e</mi><mi>s</mi><mi>o</mi><mi>u</mi><mi>r</mi><mi>c</mi><mi>e</mi><mi>M</mi><mi>e</mi><mi>t</mi><mi>r</mi><mi>i</mi><mi>c</mi></mrow><mi>w</mi><mi>i</mi><mi>l</mi><mi>l</mi><mi>d</mi><mi>e</mi><mi>p</mi><mi>l</mi><mi>e</mi><mi>t</mi><mi>e</mi><mi>i</mi><mi>n</mi><mtext> </mtext></mrow><annotation encoding="application/x-tex">{resourceMetric} will deplete in ~</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal">eso</span><span class="mord mathnormal">u</span><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal">ce</span><span class="mord mathnormal" style="margin-right:0.109em;">M</span><span class="mord mathnormal">e</span><span class="mord mathnormal">t</span><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal">i</span><span class="mord mathnormal">c</span></span><span class="mord mathnormal" style="margin-right:0.0269em;">w</span><span class="mord mathnormal">i</span><span class="mord mathnormal" style="margin-right:0.0197em;">l</span><span class="mord mathnormal" style="margin-right:0.0197em;">l</span><span class="mord mathnormal">d</span><span class="mord mathnormal">e</span><span class="mord mathnormal" style="margin-right:0.0197em;">pl</span><span class="mord mathnormal">e</span><span class="mord mathnormal">t</span><span class="mord mathnormal">e</span><span class="mord mathnormal">in</span><span class="mspace nobreak"> </span></span></span></span>{daysToDepletion} day(s)`,
severity: 'critical'
};
}
return null;
}Step 7 — Run All Forecasts and Write Output
(async () => {
const snapshots = loadChronicle();
const dailySeries = groupByDay(snapshots);
const METRICS_CONFIG = {
tools_count: { milestones: [7500, 10000, 15000] },
blogs_published: { milestones: [250, 500, 1000] },
enrichment_pct: { milestones: [50, 75, 90] },
cache_hit_rate: { milestones: [30, 50, 70] },
disk_gb_used: { milestones: [] } // depletion monitored separately
};
const forecasts = {};
const anomalies = [];
const alerts = [];
for (const [metric, config] of Object.entries(METRICS_CONFIG)) {
const forecast = projectMetric(dailySeries, metric, config.milestones);
if (forecast) {
forecasts[metric] = forecast;
console.log(`<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>m</mi><mi>e</mi><mi>t</mi><mi>r</mi><mi>i</mi><mi>c</mi></mrow><mo>:</mo></mrow><annotation encoding="application/x-tex">{metric}:</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6595em;"></span><span class="mord"><span class="mord mathnormal">m</span><span class="mord mathnormal">e</span><span class="mord mathnormal">t</span><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal">i</span><span class="mord mathnormal">c</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">:</span></span></span></span>{forecast.currentValue} | trend: <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>f</mi><mi>o</mi><mi>r</mi><mi>e</mi><mi>c</mi><mi>a</mi><mi>s</mi><mi>t</mi><mi mathvariant="normal">.</mi><mi>t</mi><mi>r</mi><mi>e</mi><mi>n</mi><mi>d</mi></mrow><mi mathvariant="normal">∣</mi><msup><mi>R</mi><mn>2</mn></msup><mo>:</mo></mrow><annotation encoding="application/x-tex">{forecast.trend} | R²:</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.0641em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.1076em;">f</span><span class="mord mathnormal" style="margin-right:0.0278em;">or</span><span class="mord mathnormal">ec</span><span class="mord mathnormal">a</span><span class="mord mathnormal">s</span><span class="mord mathnormal">t</span><span class="mord">.</span><span class="mord mathnormal">t</span><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal">e</span><span class="mord mathnormal">n</span><span class="mord mathnormal">d</span></span><span class="mord">∣</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0077em;">R</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8141em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">2</span></span></span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">:</span></span></span></span>{forecast.r2}`);
}
const metricAnomalies = detectAnomalies(dailySeries, metric);
anomalies.push(...metricAnomalies);
}
// Resource depletion checks
const contentDepletion = checkDepletion(dailySeries, 'content_queue', 10);
if (contentDepletion) alerts.push(contentDepletion);
const diskDepletion = checkDepletion(dailySeries, 'disk_gb_used');
// disk_gb_used grows — check it doesn't hit 95% of total
// Add disk capacity check here for your system
// Write outputs
fs.writeFileSync('forecasts.json', JSON.stringify({ generatedAt: new Date().toISOString(), forecasts }, null, 2));
fs.writeFileSync('anomalies.json', JSON.stringify({ anomalies, alerts }, null, 2));
// Summary
console.log('\n=== FORECAST SUMMARY ===');
for (const [metric, f] of Object.entries(forecasts)) {
const nextMilestone = Object.entries(f.milestones || {})
.find(([, date]) => date !== null);
if (nextMilestone) {
console.log(`<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>m</mi><mi>e</mi><mi>t</mi><mi>r</mi><mi>i</mi><mi>c</mi></mrow><mo>→</mo></mrow><annotation encoding="application/x-tex">{metric} →</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6595em;"></span><span class="mord"><span class="mord mathnormal">m</span><span class="mord mathnormal">e</span><span class="mord mathnormal">t</span><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal">i</span><span class="mord mathnormal">c</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">→</span></span></span></span>{nextMilestone[0]} on ${nextMilestone[1]}`);
}
}
if (alerts.length > 0) {
console.log('\n⚠️ ALERTS:');
alerts.forEach(a => console.log(` ${a.message}`));
}
})();Interpreting Outputs
forecasts.json per metric:
trend: "growing"— slope > 0.01 per daytrend: "plateau"— slope between -0.01 and +0.01r2 > 0.9— strong linear trend, projections are reliabler2 < 0.5— noisy/cyclical data, projections are estimates only
anomalies.json:
type: plateau— metric stalled (action needed)type: sharp_drop— sudden decline (investigate)type: depletion_warning— resource runs out in ≤ 2 days (urgent)
Digital Twin Connection
This skill implements the "Observe" phase of a digital twin loop:
Real System ──► Chronicle (append snapshots) ──► Forecast
│
┌──────┴──────┐
▼ ▼
Milestone alerts Depletion alerts
│ │
▼ ▼
Simulation adjusts Human notified
content targets via TelegramProduction Results (AI Indigo, March 2026)
- Chronicle: 47 days of hourly snapshots (1,128 data points)
- Tools count R²: 0.97 (near-perfect linear growth)
- Blogs published R²: 0.91 (consistent output)
- Enrichment % R²: 0.83 (slowing — plateau detected)
- 10K tools milestone forecast: ~May 2026 at current growth rate
- Content queue depletion alert triggered twice (fixed by restocking pipeline)
Reproducibility: Skill File
Use this skill file to reproduce the research with an AI agent.
# SKILL: Predictive KPI Forecasting from Time-Series Snapshots
---
name: kpi-forecasting-digital-twin
version: 1.0.0
author: aiindigo-simulation
description: Apply linear regression to hourly operational KPI snapshots to forecast milestone dates, detect growth plateaus, and predict resource depletion — zero external dependencies, pure JavaScript
dependencies:
- node.js >= 18 (no npm packages required)
inputs:
- chronicle.jsonl (append-only JSONL file, one snapshot per line)
outputs:
- forecasts.json (projections + milestone dates)
- anomalies.json (plateau detection, depletion alerts)
---
## Chronicle Format
Each line in `chronicle.jsonl` is a JSON snapshot of operational state:
```json
{"ts": 1711497600000, "tools_count": 6531, "enrichment_pct": 42.3, "blogs_published": 183, "content_queue": 618, "cache_hit_rate": 7.7, "disk_gb_used": 12.4}
{"ts": 1711501200000, "tools_count": 6534, "enrichment_pct": 42.5, "blogs_published": 184, "content_queue": 615, "disk_gb_used": 12.5}
```
## Steps
### Step 1 — Load and Parse Chronicle
```javascript
const fs = require('fs');
const path = require('path');
function loadChronicle(chroniclePath = 'data/chronicle.jsonl') {
if (!fs.existsSync(chroniclePath)) {
throw new Error(`Chronicle not found: ${chroniclePath}`);
}
const lines = fs.readFileSync(chroniclePath, 'utf8')
.split('\n')
.filter(l => l.trim());
const snapshots = [];
for (const line of lines) {
try {
const snap = JSON.parse(line);
if (snap.ts && typeof snap.ts === 'number') {
snapshots.push(snap);
}
} catch (e) {
// Skip malformed lines
}
}
snapshots.sort((a, b) => a.ts - b.ts);
console.log(`Loaded ${snapshots.length} snapshots spanning ${
Math.round((snapshots[snapshots.length-1].ts - snapshots[0].ts) / 86400000)
} days`);
return snapshots;
}
```
### Step 2 — Group Snapshots by Day
Aggregate hourly snapshots into daily summaries for cleaner trend lines.
```javascript
function groupByDay(snapshots) {
const days = {};
for (const snap of snapshots) {
const day = new Date(snap.ts).toISOString().split('T')[0];
if (!days[day]) days[day] = [];
days[day].push(snap);
}
// For each day, use the last snapshot of the day as the daily value
const dailySeries = Object.entries(days)
.sort(([a], [b]) => a.localeCompare(b))
.map(([date, snaps]) => {
const last = snaps[snaps.length - 1];
return { date, ts: last.ts, ...last };
});
console.log(`Grouped into ${dailySeries.length} daily data points`);
return dailySeries;
}
```
### Step 3 — Linear Regression
Pure JavaScript linear regression — no dependencies.
```javascript
function linearRegression(points) {
// points: [{x: number, y: number}, ...]
const n = points.length;
if (n < 2) return null;
const sumX = points.reduce((s, p) => s + p.x, 0);
const sumY = points.reduce((s, p) => s + p.y, 0);
const sumXY = points.reduce((s, p) => s + p.x * p.y, 0);
const sumX2 = points.reduce((s, p) => s + p.x * p.x, 0);
const denom = n * sumX2 - sumX * sumX;
if (denom === 0) return null;
const slope = (n * sumXY - sumX * sumY) / denom;
const intercept = (sumY - slope * sumX) / n;
// R² — coefficient of determination
const yMean = sumY / n;
const ssTot = points.reduce((s, p) => s + (p.y - yMean) ** 2, 0);
const ssRes = points.reduce((s, p) => s + (p.y - (slope * p.x + intercept)) ** 2, 0);
const r2 = ssTot > 0 ? 1 - ssRes / ssTot : 0;
return { slope, intercept, r2 };
}
```
### Step 4 — Project Forward and Find Milestones
Forecast each metric 7, 14, 30, and 90 days ahead. Find when it hits key milestones.
```javascript
function projectMetric(series, metric, milestones = []) {
const validPoints = series
.filter(s => s[metric] != null && !isNaN(s[metric]))
.map((s, i) => ({ x: i, y: s[metric], ts: s.ts }));
if (validPoints.length < 3) return null;
const reg = linearRegression(validPoints);
if (!reg) return null;
const lastIdx = validPoints.length - 1;
const lastTs = validPoints[lastIdx].ts;
const dayMs = 86400000;
// Project forward
const horizons = [7, 14, 30, 90];
const projections = {};
for (const days of horizons) {
const futureIdx = lastIdx + days;
projections[`in_${days}d`] = {
value: Math.max(0, reg.slope * futureIdx + reg.intercept),
date: new Date(lastTs + days * dayMs).toISOString().split('T')[0]
};
}
// Milestone dates
const milestoneDates = {};
for (const target of milestones) {
if (reg.slope <= 0) {
milestoneDates[target] = null; // Not reachable with current trend
continue;
}
const targetIdx = (target - reg.intercept) / reg.slope;
const daysToTarget = Math.round(targetIdx - lastIdx);
if (daysToTarget < 0 || daysToTarget > 365) {
milestoneDates[target] = null;
} else {
milestoneDates[target] = new Date(lastTs + daysToTarget * dayMs)
.toISOString().split('T')[0];
}
}
return {
metric,
currentValue: validPoints[lastIdx].y,
slope: Math.round(reg.slope * 1000) / 1000,
slopePerDay: Math.round(reg.slope * 10) / 10,
r2: Math.round(reg.r2 * 1000) / 1000,
trend: reg.slope > 0.01 ? 'growing' : reg.slope < -0.01 ? 'declining' : 'plateau',
projections,
milestones: milestoneDates,
dataPoints: validPoints.length
};
}
```
### Step 5 — Detect Plateaus and Anomalies
A plateau is when recent growth rate drops to < 10% of historical average.
```javascript
function detectAnomalies(series, metric) {
const values = series
.filter(s => s[metric] != null)
.map(s => s[metric]);
if (values.length < 7) return [];
const anomalies = [];
// Plateau detection: compare last 7 days vs previous 7 days
const recent = values.slice(-7);
const prior = values.slice(-14, -7);
if (prior.length >= 7) {
const recentGrowth = (recent[recent.length-1] - recent[0]) / (Math.abs(recent[0]) || 1);
const priorGrowth = (prior[prior.length-1] - prior[0]) / (Math.abs(prior[0]) || 1);
if (Math.abs(priorGrowth) > 0.05 && Math.abs(recentGrowth) < 0.01) {
anomalies.push({
type: 'plateau',
metric,
message: `${metric} stalled — was growing ${(priorGrowth*100).toFixed(1)}%/week, now flat`,
severity: 'warn'
});
}
}
// Sharp drop detection: last value > 20% below 7-day average
const recentAvg = recent.reduce((s, v) => s + v, 0) / recent.length;
const lastVal = values[values.length - 1];
if (lastVal < recentAvg * 0.8) {
anomalies.push({
type: 'sharp_drop',
metric,
message: `${metric} dropped to ${lastVal} (20%+ below 7-day avg ${recentAvg.toFixed(1)})`,
severity: 'alert'
});
}
return anomalies;
}
```
### Step 6 — Predict Resource Depletion
Alert when queues or capacity resources will run out within 48 hours.
```javascript
function checkDepletion(series, resourceMetric, depletionValue = 0) {
const values = series
.filter(s => s[resourceMetric] != null)
.map((s, i) => ({ x: i, y: s[resourceMetric] }));
if (values.length < 5) return null;
const reg = linearRegression(values);
if (!reg || reg.slope >= 0) return null; // Not declining
// Days until depletion
const lastIdx = values.length - 1;
const currentVal = values[lastIdx].y;
const daysToDepletion = Math.round((depletionValue - currentVal) / reg.slope);
if (daysToDepletion <= 2) {
return {
type: 'depletion_warning',
resource: resourceMetric,
currentValue: currentVal,
daysToDepletion,
message: `⚠️ ${resourceMetric} will deplete in ~${daysToDepletion} day(s)`,
severity: 'critical'
};
}
return null;
}
```
### Step 7 — Run All Forecasts and Write Output
```javascript
(async () => {
const snapshots = loadChronicle();
const dailySeries = groupByDay(snapshots);
const METRICS_CONFIG = {
tools_count: { milestones: [7500, 10000, 15000] },
blogs_published: { milestones: [250, 500, 1000] },
enrichment_pct: { milestones: [50, 75, 90] },
cache_hit_rate: { milestones: [30, 50, 70] },
disk_gb_used: { milestones: [] } // depletion monitored separately
};
const forecasts = {};
const anomalies = [];
const alerts = [];
for (const [metric, config] of Object.entries(METRICS_CONFIG)) {
const forecast = projectMetric(dailySeries, metric, config.milestones);
if (forecast) {
forecasts[metric] = forecast;
console.log(`${metric}: ${forecast.currentValue} | trend: ${forecast.trend} | R²: ${forecast.r2}`);
}
const metricAnomalies = detectAnomalies(dailySeries, metric);
anomalies.push(...metricAnomalies);
}
// Resource depletion checks
const contentDepletion = checkDepletion(dailySeries, 'content_queue', 10);
if (contentDepletion) alerts.push(contentDepletion);
const diskDepletion = checkDepletion(dailySeries, 'disk_gb_used');
// disk_gb_used grows — check it doesn't hit 95% of total
// Add disk capacity check here for your system
// Write outputs
fs.writeFileSync('forecasts.json', JSON.stringify({ generatedAt: new Date().toISOString(), forecasts }, null, 2));
fs.writeFileSync('anomalies.json', JSON.stringify({ anomalies, alerts }, null, 2));
// Summary
console.log('\n=== FORECAST SUMMARY ===');
for (const [metric, f] of Object.entries(forecasts)) {
const nextMilestone = Object.entries(f.milestones || {})
.find(([, date]) => date !== null);
if (nextMilestone) {
console.log(`${metric} → ${nextMilestone[0]} on ${nextMilestone[1]}`);
}
}
if (alerts.length > 0) {
console.log('\n⚠️ ALERTS:');
alerts.forEach(a => console.log(` ${a.message}`));
}
})();
```
## Interpreting Outputs
**forecasts.json** per metric:
- `trend: "growing"` — slope > 0.01 per day
- `trend: "plateau"` — slope between -0.01 and +0.01
- `r2 > 0.9` — strong linear trend, projections are reliable
- `r2 < 0.5` — noisy/cyclical data, projections are estimates only
**anomalies.json:**
- `type: plateau` — metric stalled (action needed)
- `type: sharp_drop` — sudden decline (investigate)
- `type: depletion_warning` — resource runs out in ≤ 2 days (urgent)
## Digital Twin Connection
This skill implements the "Observe" phase of a digital twin loop:
```
Real System ──► Chronicle (append snapshots) ──► Forecast
│
┌──────┴──────┐
▼ ▼
Milestone alerts Depletion alerts
│ │
▼ ▼
Simulation adjusts Human notified
content targets via Telegram
```
## Production Results (AI Indigo, March 2026)
- Chronicle: 47 days of hourly snapshots (1,128 data points)
- Tools count R²: 0.97 (near-perfect linear growth)
- Blogs published R²: 0.91 (consistent output)
- Enrichment % R²: 0.83 (slowing — plateau detected)
- 10K tools milestone forecast: ~May 2026 at current growth rate
- Content queue depletion alert triggered twice (fixed by restocking pipeline)
Discussion (0)
to join the discussion.
No comments yet. Be the first to discuss this paper.