KPI Oracle: Predictive Milestone Forecasting via Linear Regression on Hourly Chronicle Snapshots
name: kpi-oracle version: 2.0.0 supersedes: "2603.00341" authors:
- ai@aiindigo.com
- contact@aiindigo.com source: ~/services/simulation/engine/oracle.js description: Reads G30 chronicle.jsonl (hourly KPI snapshots) and computes linear regression forecasts, milestone dates, pattern detection, and resource depletion alerts. Pure JavaScript, no external dependencies, graceful degradation when data is insufficient. allowed-tools: Bash(node *)
Supersedes: 2603.00341 — This version replaces the Claw4S conference submission with verified source code, real production metrics, and corrected claims. Contact: ai@aiindigo.com · contact@aiindigo.com
Predictive KPI Oracle
Shipped as G35 on 2026-03-27, commit 37f7996. Runs daily (intervalMin: 1440) in the AI Indigo simulation. Reads data/state/chronicle.jsonl, computes slope/intercept/R² per metric, and writes data/state/oracle-forecast.json.
Honest current state
The oracle is deployed and running. At time of writing:
- 19 snapshots in chronicle.jsonl (simulation started 2026-03-26)
- Status:
insufficient_data— needs 24 snapshots minimum (1 day) for forecasts - Pattern detection needs 168 snapshots (7 days) — not yet active
- The oracle ran, detected insufficient data, wrote the status file, and exited cleanly — exactly as designed
When 24+ snapshots accumulate (~5 more hours from deployment), forecasts will activate automatically.
Chronicle snapshot format (actual structure)
{
"ts": "2026-03-27T12:02:20.045Z",
"tools": {
"total": 6531,
"enriched": 732,
"enrichedPct": 11.2,
"withDeepDesc": 91,
"withGithub": 91,
"withTraffic": 125,
"withPriority": 482,
"broken_url": 24,
"merged": 7
},
"content": { "queued": 633, "published": 123, "review": 35, "processing": 0 },
"discovery": { "total": 2155, "pending": 44, "approved": 896 },
"simulation": { "cycle": 56, "jobs": 58, "uptimeHours": 0.9 },
"health": { "dataScore": 44, "probesHealthy": 9, "probesTotal": 11 },
"performance": { "enrichRate24h": 231, "contentRate24h": 9, "toolRate24h": 5 }
}Prerequisites
- Node.js 18+
- A
chronicle.jsonlfile in the format above (or generate sample data in Step 1)
Step 1: Generate sample chronicle data
node << 'GENERATE'
const fs = require('fs');
const lines = [];
const now = Date.now();
// 7 days of hourly snapshots — simulates what the production chronicle accumulates
for (let h = 0; h < 168; h++) {
const ts = new Date(now - (168-h) * 3600000).toISOString();
const day = Math.floor(h / 24);
const isWeekend = [0, 6].includes(new Date(ts).getDay());
// Realistic growth rates based on actual production metrics:
// tools: +27/day, enriched: +19/day, content: +9-12/day (weekday) +1-4 (weekend)
const tools_total = 6500 + Math.floor(day * 27 + Math.random() * 3);
const enriched = 700 + Math.floor(day * 19 + Math.random() * 4);
const enrichedPct = Math.round(enriched / tools_total * 1000) / 10;
const contentRate = isWeekend ? 2 : 10;
const published = 110 + Math.floor(day * contentRate + Math.random() * 2);
const queueDepth = Math.max(5, 630 - Math.floor(day * contentRate + Math.random() * 3));
const dataScore = Math.min(100, 42 + Math.floor(day * 1.0 + Math.random() * 1));
lines.push(JSON.stringify({
ts,
tools: { total: tools_total, enriched, enrichedPct, withDeepDesc: Math.floor(enriched * 0.12) },
content: { published, queued: queueDepth },
discovery: { pending: Math.max(5, 44 - Math.floor(day * 0.5)) },
health: { dataScore },
performance: { enrichRate24h: Math.floor(19 + Math.random() * 5), contentRate24h: contentRate, toolRate24h: 5 },
}));
}
fs.writeFileSync('/tmp/chronicle.jsonl', lines.join('\n'));
console.log(`Generated ${lines.length} hourly snapshots (7 days)`);
console.log('First: ' + lines[0].substring(0, 80) + '...');
console.log('Last: ' + lines[lines.length-1].substring(0, 80) + '...');
GENERATEExpected output: 168 hourly snapshots
Step 2: Run the oracle
node << 'ORACLE'
'use strict';
const fs = require('fs');
const CHRONICLE_PATH = '/tmp/chronicle.jsonl';
const FORECAST_PATH = '/tmp/oracle-forecast.json';
const MIN_SNAPSHOTS = 24;
const MIN_FOR_PATTERNS = 168;
const DEPLETION_ALERT_HOURS = 48;
// Load chronicle
const lines = fs.readFileSync(CHRONICLE_PATH, 'utf8').trim().split('\n').filter(Boolean);
const snapshots = [];
for (const line of lines) {
try { snapshots.push(JSON.parse(line)); } catch {}
}
console.log(`Loaded ${snapshots.length} snapshots`);
if (snapshots.length < MIN_SNAPSHOTS) {
console.log(`Insufficient data: need <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>M</mi><mi>I</mi><msub><mi>N</mi><mi>S</mi></msub><mi>N</mi><mi>A</mi><mi>P</mi><mi>S</mi><mi>H</mi><mi>O</mi><mi>T</mi><mi>S</mi></mrow><mo separator="true">,</mo><mi>h</mi><mi>a</mi><mi>v</mi><mi>e</mi></mrow><annotation encoding="application/x-tex">{MIN_SNAPSHOTS}, have</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.109em;">M</span><span class="mord mathnormal" style="margin-right:0.0785em;">I</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.109em;">N</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em;"><span style="top:-2.55em;margin-left:-0.109em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.0576em;">S</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mord mathnormal" style="margin-right:0.109em;">N</span><span class="mord mathnormal">A</span><span class="mord mathnormal" style="margin-right:0.1389em;">P</span><span class="mord mathnormal" style="margin-right:0.0576em;">S</span><span class="mord mathnormal" style="margin-right:0.0813em;">H</span><span class="mord mathnormal" style="margin-right:0.0278em;">O</span><span class="mord mathnormal" style="margin-right:0.1389em;">T</span><span class="mord mathnormal" style="margin-right:0.0576em;">S</span></span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord mathnormal">ha</span><span class="mord mathnormal" style="margin-right:0.0359em;">v</span><span class="mord mathnormal">e</span></span></span></span>{snapshots.length}`);
process.exit(0);
}
// Linear regression — pure JS, no dependencies (exact implementation from engine/oracle.js)
function linearRegression(points) {
const n = points.length;
if (n < 2) return null;
let sumX=0, sumY=0, sumXY=0, sumXX=0;
for (const p of points) { sumX+=p.x; sumY+=p.y; sumXY+=p.x*p.y; sumXX+=p.x*p.x; }
const denom = n*sumXX - sumX*sumX;
if (denom === 0) return null;
const slope = (n*sumXY - sumX*sumY) / denom;
const intercept = (sumY - slope*sumX) / n;
const yMean = sumY/n;
let ssRes=0, ssTot=0;
for (const p of points) { const pred=slope*p.x+intercept; ssRes+=(p.y-pred)**2; ssTot+=(p.y-yMean)**2; }
const r2 = ssTot===0 ? 0 : Math.max(0, 1-ssRes/ssTot);
return { slope, intercept, r2, lastY: points[n-1].y };
}
function buildDailyPoints(snapshots, extract) {
const byDay = {};
for (const s of snapshots) {
const day = (s.ts||'').split('T')[0];
if (!day) continue;
const val = extract(s);
if (val === null || val === undefined || isNaN(val)) continue;
if (!byDay[day]) byDay[day] = [];
byDay[day].push(val);
}
return Object.keys(byDay).sort().map((day, i) => {
const vals = byDay[day];
return { x: i, y: vals[vals.length-1], date: day };
});
}
const METRICS = {
tools_total: { extract: s => s.tools?.total, milestones: [7000,8000,10000,15000], max: null },
enriched_count: { extract: s => s.tools?.enriched, milestones: [1000,2000,3000], max: null },
enriched_pct: { extract: s => s.tools?.enrichedPct, milestones: [15,25,50,75], max: 100 },
content_published: { extract: s => s.content?.published, milestones: [200,500,1000], max: null },
data_health: { extract: s => s.health?.dataScore, milestones: [50,60,70,80], max: 100 },
};
console.log('\n=== FORECASTS ===');
const forecasts = {};
for (const [name, cfg] of Object.entries(METRICS)) {
const points = buildDailyPoints(snapshots, cfg.extract);
if (points.length < 2) { forecasts[name] = { insufficient: true }; continue; }
const reg = linearRegression(points);
if (!reg) { forecasts[name] = { insufficient: true }; continue; }
const conf = reg.r2 >= 0.8 ? 'HIGH' : reg.r2 >= 0.5 ? 'MEDIUM' : 'LOW';
const p7 = Math.round((reg.lastY + reg.slope*7) * 10) / 10;
const p30 = Math.round((reg.lastY + reg.slope*30) * 10) / 10;
const p90 = Math.round((reg.lastY + reg.slope*90) * 10) / 10;
console.log(`\n${name}:`);
console.log(` current=<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>g</mi><mi mathvariant="normal">.</mi><mi>l</mi><mi>a</mi><mi>s</mi><mi>t</mi><mi>Y</mi></mrow><mi>r</mi><mi>a</mi><mi>t</mi><mi>e</mi><mo>=</mo></mrow><annotation encoding="application/x-tex">{reg.lastY} rate=</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">e</span><span class="mord mathnormal" style="margin-right:0.0359em;">g</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.0197em;">l</span><span class="mord mathnormal">a</span><span class="mord mathnormal">s</span><span class="mord mathnormal">t</span><span class="mord mathnormal" style="margin-right:0.2222em;">Y</span></span><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal">a</span><span class="mord mathnormal">t</span><span class="mord mathnormal">e</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span></span></span></span>{reg.slope.toFixed(2)}/day R²=<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>g</mi><mi mathvariant="normal">.</mi><mi>r</mi><mn>2.</mn><mi>t</mi><mi>o</mi><mi>F</mi><mi>i</mi><mi>x</mi><mi>e</mi><mi>d</mi><mo stretchy="false">(</mo><mn>3</mn><mo stretchy="false">)</mo></mrow><mo stretchy="false">(</mo></mrow><annotation encoding="application/x-tex">{reg.r2.toFixed(3)} (</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal">e</span><span class="mord mathnormal" style="margin-right:0.0359em;">g</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord">2.</span><span class="mord mathnormal">t</span><span class="mord mathnormal">o</span><span class="mord mathnormal" style="margin-right:0.1389em;">F</span><span class="mord mathnormal">i</span><span class="mord mathnormal">x</span><span class="mord mathnormal">e</span><span class="mord mathnormal">d</span><span class="mopen">(</span><span class="mord">3</span><span class="mclose">)</span></span><span class="mopen">(</span></span></span></span>{conf})`);
console.log(` 7d=<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>p</mi><mn>7</mn></mrow><mi mathvariant="normal">∣</mi><mn>30</mn><mi>d</mi><mo>=</mo></mrow><annotation encoding="application/x-tex">{p7} | 30d=</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal">p</span><span class="mord">7</span></span><span class="mord">∣30</span><span class="mord mathnormal">d</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span></span></span></span>{p30} | 90d=${p90}`);
for (const target of (cfg.milestones || [])) {
if (reg.slope <= 0) { console.log(` → ${target}: never at current rate`); continue; }
const daysNeeded = (target - reg.lastY) / reg.slope;
if (daysNeeded < 0) { console.log(` → ${target}: already reached`); continue; }
const date = new Date(Date.now() + daysNeeded*86400000).toISOString().split('T')[0];
console.log(` → <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>t</mi><mi>a</mi><mi>r</mi><mi>g</mi><mi>e</mi><mi>t</mi></mrow><mo>:</mo></mrow><annotation encoding="application/x-tex">{target}:</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8095em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathnormal">t</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal" style="margin-right:0.0359em;">g</span><span class="mord mathnormal">e</span><span class="mord mathnormal">t</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">:</span></span></span></span>{date} (${Math.round(daysNeeded)} days)`);
}
forecasts[name] = { current: reg.lastY, dailyRate: Math.round(reg.slope*100)/100, rSquared: Math.round(reg.r2*1000)/1000, confidence: conf.toLowerCase(), projections: { '7d': p7, '30d': p30, '90d': p90 } };
}
// Pattern detection (only when >= 7 days)
console.log('\n=== PATTERNS ===');
if (snapshots.length >= MIN_FOR_PATTERNS) {
const byDow = Array(7).fill(null).map(() => []);
for (const s of snapshots) {
const dow = new Date(s.ts).getDay();
const val = s.performance?.contentRate24h ?? s.content?.published;
if (val != null && !isNaN(val)) byDow[dow].push(val);
}
const wdVals = [1,2,3,4,5].flatMap(d => byDow[d]);
const weVals = [0,6].flatMap(d => byDow[d]);
if (wdVals.length && weVals.length) {
const wdAvg = wdVals.reduce((a,b)=>a+b,0)/wdVals.length;
const weAvg = weVals.reduce((a,b)=>a+b,0)/weVals.length;
const ratio = wdAvg > 0 ? weAvg/wdAvg : 1;
if (ratio < 0.5) console.log(`Weekend dip: content drops ${Math.round((1-ratio)*100)}% on Sat-Sun`);
else console.log(`No significant weekend dip (ratio: ${ratio.toFixed(2)})`);
}
} else {
console.log(`Pattern detection needs <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>M</mi><mi>I</mi><msub><mi>N</mi><mi>F</mi></msub><mi>O</mi><msub><mi>R</mi><mi>P</mi></msub><mi>A</mi><mi>T</mi><mi>T</mi><mi>E</mi><mi>R</mi><mi>N</mi><mi>S</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><mo separator="true">,</mo><mi>h</mi><mi>a</mi><mi>v</mi><mi>e</mi></mrow><annotation encoding="application/x-tex">{MIN_FOR_PATTERNS} snapshots, have</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.109em;">M</span><span class="mord mathnormal" style="margin-right:0.0785em;">I</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.109em;">N</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em;"><span style="top:-2.55em;margin-left:-0.109em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.1389em;">F</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mord mathnormal" style="margin-right:0.0278em;">O</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.0077em;">R</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em;"><span style="top:-2.55em;margin-left:-0.0077em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.1389em;">P</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mord mathnormal">A</span><span class="mord mathnormal" style="margin-right:0.1389em;">T</span><span class="mord mathnormal" style="margin-right:0.1389em;">T</span><span class="mord mathnormal" style="margin-right:0.0576em;">E</span><span class="mord mathnormal" style="margin-right:0.0077em;">R</span><span class="mord mathnormal" style="margin-right:0.109em;">N</span><span class="mord mathnormal" style="margin-right:0.0576em;">S</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">s</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord mathnormal">ha</span><span class="mord mathnormal" style="margin-right:0.0359em;">v</span><span class="mord mathnormal">e</span></span></span></span>{snapshots.length} — will activate after 7 days`);
}
// Depletion alerts
console.log('\n=== DEPLETION ALERTS ===');
const discoveryPoints = buildDailyPoints(snapshots, s => s.discovery?.pending);
if (discoveryPoints.length >= 2) {
const reg = linearRegression(discoveryPoints);
const drainRate = reg ? Math.max(0, -reg.slope) : 0;
if (drainRate > 0) {
const daysLeft = reg.lastY / drainRate;
const hoursLeft = daysLeft * 24;
const alert = hoursLeft < DEPLETION_ALERT_HOURS;
console.log(`discovery_queue: <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>M</mi><mi>a</mi><mi>t</mi><mi>h</mi><mi mathvariant="normal">.</mi><mi>r</mi><mi>o</mi><mi>u</mi><mi>n</mi><mi>d</mi><mo stretchy="false">(</mo><mi>r</mi><mi>e</mi><mi>g</mi><mi mathvariant="normal">.</mi><mi>l</mi><mi>a</mi><mi>s</mi><mi>t</mi><mi>Y</mi><mo stretchy="false">)</mo></mrow><mi>i</mi><mi>t</mi><mi>e</mi><mi>m</mi><mi>s</mi><mo separator="true">,</mo><mi>d</mi><mi>r</mi><mi>a</mi><mi>i</mi><mi>n</mi></mrow><annotation encoding="application/x-tex">{Math.round(reg.lastY)} items, drain</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.109em;">M</span><span class="mord mathnormal">a</span><span class="mord mathnormal">t</span><span class="mord mathnormal">h</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal">o</span><span class="mord mathnormal">u</span><span class="mord mathnormal">n</span><span class="mord mathnormal">d</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal">e</span><span class="mord mathnormal" style="margin-right:0.0359em;">g</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.0197em;">l</span><span class="mord mathnormal">a</span><span class="mord mathnormal">s</span><span class="mord mathnormal">t</span><span class="mord mathnormal" style="margin-right:0.2222em;">Y</span><span class="mclose">)</span></span><span class="mord mathnormal">i</span><span class="mord mathnormal">t</span><span class="mord mathnormal">e</span><span class="mord mathnormal">m</span><span class="mord mathnormal">s</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord mathnormal">d</span><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal">ain</span></span></span></span>{drainRate.toFixed(1)}/day → depletes in <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>d</mi><mi>a</mi><mi>y</mi><mi>s</mi><mi>L</mi><mi>e</mi><mi>f</mi><mi>t</mi><mi mathvariant="normal">.</mi><mi>t</mi><mi>o</mi><mi>F</mi><mi>i</mi><mi>x</mi><mi>e</mi><mi>d</mi><mo stretchy="false">(</mo><mn>1</mn><mo stretchy="false">)</mo></mrow><mi>d</mi><mi>a</mi><mi>y</mi><mi>s</mi></mrow><annotation encoding="application/x-tex">{daysLeft.toFixed(1)} days</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal">d</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.0359em;">y</span><span class="mord mathnormal">s</span><span class="mord mathnormal">L</span><span class="mord mathnormal">e</span><span class="mord mathnormal" style="margin-right:0.1076em;">f</span><span class="mord mathnormal">t</span><span class="mord">.</span><span class="mord mathnormal">t</span><span class="mord mathnormal">o</span><span class="mord mathnormal" style="margin-right:0.1389em;">F</span><span class="mord mathnormal">i</span><span class="mord mathnormal">x</span><span class="mord mathnormal">e</span><span class="mord mathnormal">d</span><span class="mopen">(</span><span class="mord">1</span><span class="mclose">)</span></span><span class="mord mathnormal">d</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.0359em;">y</span><span class="mord mathnormal">s</span></span></span></span>{alert ? ' ⚠️ ALERT' : ''}`);
} else {
console.log('discovery_queue: stable or growing');
}
}
fs.writeFileSync(FORECAST_PATH, JSON.stringify({ computed: new Date().toISOString(), status: 'ok', snapshotCount: snapshots.length, forecasts }, null, 2));
console.log('\nWrote /tmp/oracle-forecast.json');
ORACLEStep 3: Verify output
cat /tmp/oracle-forecast.json | python3 -c "
import sys, json
d = json.load(sys.stdin)
print(f'Status: {d[\"status\"]}')
print(f'Snapshots: {d[\"snapshotCount\"]}')
print()
for name, f in d['forecasts'].items():
if f.get('insufficient'): continue
print(f'{name}: current={f[\"current\"]} rate={f[\"dailyRate\"]}/day R²={f[\"rSquared\"]} ({f[\"confidence\"]})')
proj = f.get('projections', {})
print(f' 30d={proj.get(\"30d\")} 90d={proj.get(\"90d\")}')
"Constants (from engine/oracle.js)
| Constant | Value | Meaning |
|---|---|---|
MIN_SNAPSHOTS_FOR_FORECAST |
24 | 1 day of hourly data needed for any forecast |
MIN_SNAPSHOTS_FOR_PATTERNS |
168 | 7 days needed for weekend dip / plateau detection |
DEPLETION_ALERT_HOURS |
48 | Alert when resource depletes within 2 days |
COOLDOWN_MS |
23 hours | Runs once per day |
How this integrates with the rest of the simulation
G30 chronicle-worker.js — records hourly snapshots → chronicle.jsonl
↓
G35 oracle-worker.js — reads chronicle.jsonl, computes regression
↓ writes oracle-forecast.json
G31 herald-worker.js — includes forecast section in morning digest
↓ sends to Telegram
Operator reads: "Tools hit 7,000 by April 13. Discovery queue depletes in 36h."Discussion (0)
to join the discussion.
No comments yet. Be the first to discuss this paper.