Archive ingestion had been dead since 2026-08-02 because nothing in the compose stack actually ran it. Everything downstream starved from there. - add ingest + enrichment services. server.js only starts the scheduler when DURIIN_RUN_SCHEDULER is not "false", and workers/index.js was not running at all, so articles never got event_id/content/has_embedding and the coordinator had nothing to lease. - pass an explicit origin from coordinatorWorker. it was never passed, so acceptProposal defaulted to 'live' and 464 historical backfill predictions were recorded as live. that also meant verifyEvidence got a null cutoff and skipped its date check entirely. - coarsen cohortKey to event families + horizon buckets. 201 free text event types produced 221 cohorts averaging 2.76 samples, so the n>=30 gate could never be reached and everything abstained for the wrong reason. - gate on cohort diversity, not just sample count. one ticker was roughly half of all resolved outcomes, so a pure count gate was measuring one company. unknown diversity abstains rather than passing. - resolve the admin archive db explicitly and probe it. it relied on a Dockerfile symlink, and without it better-sqlite3 quietly creates an empty file and serves a phantom archive. - clamp implausible future publication dates at ingest. - pin the db backend to sqlite by default. compose hardcoded postgres "true", which would have overridden the operator's own .env on the next redeploy and pointed everything at a stale snapshot. scripts/repair-autonomy-labels.js relabels the affected rows. it is dry run by default and has not been applied. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WnNxwxfXSbeNtjvtz5gayb
186 lines
10 KiB
JavaScript
186 lines
10 KiB
JavaScript
(function () {
|
|
const byId = id => document.getElementById(id);
|
|
const count = (rows, key, value) => Number((rows || []).find(row => row[key] === value)?.count || 0);
|
|
const sum = (rows, predicate) => (rows || []).filter(predicate).reduce((total, row) => total + Number(row.count || 0), 0);
|
|
|
|
function renderHypotheses(rows) {
|
|
const host = byId("hypothesis-list");
|
|
if (!rows?.length) {
|
|
host.innerHTML = '<div class="empty-state">No autonomy hypotheses yet. The coordinator is working through the evidence queue.</div>';
|
|
return;
|
|
}
|
|
host.innerHTML = rows.slice(0, 7).map(row => {
|
|
const action = row.action || (row.status === "resolved" ? "MEASURED" : "OPEN");
|
|
const direction = row.direction === "positive" ? "positive" : "negative";
|
|
return `<article class="hypothesis-row">
|
|
<div><div class="hypothesis-symbol">${escapeHtml(row.instrument)}</div><div class="direction-tag ${direction}">${escapeHtml(row.direction)}</div></div>
|
|
<div><div class="hypothesis-type">${escapeHtml(String(row.event_type || "event").replaceAll("_", " "))}</div><div class="hypothesis-channel">${escapeHtml(row.causal_channel || "Evidence-backed market hypothesis")}</div></div>
|
|
<div class="hypothesis-meta">${row.evidence_count || 0} evidence source${row.evidence_count === 1 ? "" : "s"}<br>${row.horizon_days} trading-day horizon<br>${formatRelative(row.created_at)}</div>
|
|
<span class="decision-chip ${String(action).toLowerCase()}">${escapeHtml(action)}</span>
|
|
</article>`;
|
|
}).join("");
|
|
}
|
|
|
|
function renderLedger(rows) {
|
|
const host = byId("decision-ledger");
|
|
const decisions = (rows || []).filter(row => row.action);
|
|
if (!decisions.length) return;
|
|
host.innerHTML = decisions.map(row => `<tr>
|
|
<td class="mono">${escapeHtml(row.instrument)}</td>
|
|
<td><span class="decision-chip ${String(row.action).toLowerCase()}">${escapeHtml(row.action)}</span></td>
|
|
<td class="${row.direction === "positive" ? "positive" : "negative"}">${escapeHtml(row.direction)}</td>
|
|
<td class="mono">${formatPercent(row.calibrated_probability)}</td>
|
|
<td>${row.horizon_days}d</td>
|
|
<td class="muted">${formatRelative(row.created_at)}</td>
|
|
</tr>`).join("");
|
|
}
|
|
|
|
const ORIGIN_LABELS = {
|
|
live: "Live",
|
|
historical: "Historical backfill",
|
|
replay: "Walk-forward replay",
|
|
};
|
|
|
|
// live, historical and replay are deliberately never blended. only the live
|
|
// row is an edge claim, the other two are how the model was taught.
|
|
function renderOriginSplit(byOrigin, livePredictions) {
|
|
const host = byId("origin-split");
|
|
if (!host) return;
|
|
|
|
const rows = (byOrigin || []).filter(row => row.origin !== "live");
|
|
if (!rows.length) {
|
|
host.innerHTML = "";
|
|
return;
|
|
}
|
|
|
|
host.innerHTML = rows.map(row => {
|
|
const total = Number(row.total || 0);
|
|
const label = ORIGIN_LABELS[row.origin] || row.origin;
|
|
const accuracy = total ? formatPercent(Number(row.correct || 0) / total) : "—";
|
|
return `<div><span>${escapeHtml(label)}</span><strong>${formatNumber(total)} measured · ${accuracy}</strong></div>`;
|
|
}).join("");
|
|
|
|
byId("origin-note").textContent = livePredictions
|
|
? "Historical backfill and walk-forward replay are listed separately. Neither counts toward live edge."
|
|
: "No live predictions exist yet, so the numbers above are training and replay only — not evidence of live edge.";
|
|
}
|
|
|
|
function cohortCell(check, format) {
|
|
if (!check || !check.known) return '<td class="mono muted">unknown</td>';
|
|
return `<td class="mono ${check.ok ? "positive" : "negative"}">${format(check.value)} / ${format(check.threshold)}</td>`;
|
|
}
|
|
|
|
function renderCohorts(rows) {
|
|
const host = byId("cohort-list");
|
|
if (!host) return;
|
|
if (!rows?.length) {
|
|
host.innerHTML = '<tr><td colspan="6" class="empty-state">No calibration snapshots yet.</td></tr>';
|
|
return;
|
|
}
|
|
|
|
host.innerHTML = rows.map(row => {
|
|
const checks = row.qualification?.checks || {};
|
|
const qualified = Boolean(row.qualification?.qualified);
|
|
const reasons = row.qualification?.reasons || [];
|
|
const status = qualified
|
|
? '<span class="decision-chip qualifies">QUALIFIES</span>'
|
|
: `<span class="decision-chip abstain">ABSTAIN</span><div class="hypothesis-channel">${escapeHtml(reasons.join(" · ") || "does not qualify")}</div>`;
|
|
|
|
const key = row.legacy_cohort_key
|
|
? `${escapeHtml(row.cohort_key || "—")}<div class="hypothesis-channel">legacy key, not comparable to current cohorts</div>`
|
|
: escapeHtml(row.cohort_key || "—");
|
|
|
|
return `<tr>
|
|
<td class="mono">${key}</td>
|
|
<td class="muted">${escapeHtml(row.source || "unknown")}</td>
|
|
${cohortCell(checks.sample_size, value => formatNumber(value))}
|
|
${cohortCell(checks.distinct_instruments, value => formatNumber(value))}
|
|
${cohortCell(checks.top_instrument_share, value => formatPercent(value, 0))}
|
|
<td>${status}</td>
|
|
</tr>`;
|
|
}).join("");
|
|
}
|
|
|
|
function render(data) {
|
|
if (!data.enabled) throw new Error(data.reason || "Autonomy is unavailable");
|
|
const mode = String(data.mode || "shadow").toUpperCase();
|
|
const open = count(data.predictionCounts, "status", "open");
|
|
const resolved = count(data.predictionCounts, "status", "resolved");
|
|
const byOrigin = data.outcomesByOrigin || [];
|
|
const livePredictions = (data.predictionsByOrigin || []).filter(row => row.origin === "live")
|
|
.reduce((total, row) => total + Number(row.count || 0), 0);
|
|
const outcomes = Number(data.outcomes?.total || 0);
|
|
const correct = Number(data.outcomes?.correct || 0);
|
|
const accuracy = outcomes ? correct / outcomes : null;
|
|
const pendingHistorical = count((data.jobs || []).filter(row => row.lane === "historical"), "status", "pending");
|
|
const completedJobs = sum(data.jobs, row => row.status === "complete");
|
|
const proposals = sum(data.proposalCounts, row => row.status === "accepted");
|
|
const decisions = sum(data.decisionCounts, () => true);
|
|
|
|
byId("execution-mode").textContent = mode;
|
|
byId("ledger-mode").textContent = mode;
|
|
byId("runtime-execution").textContent = mode;
|
|
byId("broker-state").textContent = data.broker?.configured ? `${data.broker.name} connected` : "Broker credentials unavailable";
|
|
byId("runtime-state").textContent = `${mode} runtime active`;
|
|
byId("hero-title").textContent = mode === "PAPER" ? "Duriin is trading in simulation." : "Duriin is learning before it acts.";
|
|
byId("hero-description").textContent = mode === "PAPER"
|
|
? "Every order is backed by measured evidence, empirical calibration and deterministic risk policy. No model has direct execution authority."
|
|
: "It is turning evidence into hypotheses, waiting for outcomes, and calibrating its judgment without placing broker orders.";
|
|
|
|
byId("metric-open").textContent = formatNumber(open);
|
|
byId("metric-resolved").textContent = `${formatNumber(resolved)} resolved`;
|
|
byId("metric-accuracy").textContent = formatPercent(accuracy);
|
|
byId("metric-sample").textContent = outcomes
|
|
? `${formatNumber(outcomes)} measured live outcomes`
|
|
: (livePredictions ? "Live predictions have not matured yet" : "No live predictions yet");
|
|
byId("metric-alpha").textContent = formatPercent(data.outcomes?.average_excess_return, 2);
|
|
byId("metric-universe").textContent = formatNumber(data.allowlistedInstruments, true);
|
|
|
|
byId("pipe-observe").textContent = formatNumber(completedJobs, true);
|
|
byId("pipe-propose").textContent = formatNumber(proposals, true);
|
|
byId("pipe-measure").textContent = formatNumber(outcomes, true);
|
|
byId("pipe-calibrate").textContent = formatNumber(data.calibration?.length || 0);
|
|
byId("pipe-act").textContent = formatNumber(decisions);
|
|
byId("freshness").textContent = `Updated ${formatRelative(data.generatedAt)}`;
|
|
|
|
byId("orbit-value").textContent = formatPercent(accuracy, 0);
|
|
byId("accuracy-orbit").style.setProperty("--accuracy", `${(accuracy || 0) * 360}deg`);
|
|
byId("perf-resolved").textContent = formatNumber(outcomes);
|
|
byId("perf-correct").textContent = formatNumber(correct);
|
|
byId("perf-cohorts").textContent = formatNumber(data.calibration?.length || 0);
|
|
if (outcomes) {
|
|
byId("performance-note").textContent = `Measured on ${outcomes} matured live predictions. Results remain descriptive until the sample is large enough for stable calibration.`;
|
|
} else if (livePredictions) {
|
|
byId("performance-note").textContent = `No live prediction has matured yet — ${formatNumber(livePredictions)} are still open. Nothing here is a live track record.`;
|
|
} else {
|
|
byId("performance-note").textContent = "No live predictions yet. Everything measured so far is historical backfill or replay, which is training, not a live track record.";
|
|
}
|
|
|
|
renderOriginSplit(byOrigin, livePredictions);
|
|
renderCohorts(data.calibration);
|
|
|
|
const replay = data.replay;
|
|
byId("replay-status").textContent = replay ? replay.status : "Not started";
|
|
byId("replay-articles").textContent = replay ? formatNumber(replay.processed_articles || 0) : "—";
|
|
byId("replay-evaluations").textContent = replay ? formatNumber(replay.evaluations || 0) : "—";
|
|
byId("replay-watermark").textContent = replay?.watermark_at ? formatRelative(replay.watermark_at) : "—";
|
|
|
|
byId("runtime-queue").textContent = `${formatNumber(pendingHistorical, true)} pending`;
|
|
byId("runtime-coordinator").textContent = pendingHistorical ? "Processing" : "Watching";
|
|
if (data.account) {
|
|
byId("account-equity").textContent = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0 }).format(data.account.equity || 0);
|
|
byId("account-meta").textContent = `${data.account.broker} · sampled ${formatRelative(data.account.captured_at)}`;
|
|
}
|
|
|
|
renderHypotheses(data.latestPredictions);
|
|
renderLedger(data.latestPredictions);
|
|
}
|
|
|
|
api("/admin/api/autonomy/overview").then(render).catch(error => {
|
|
byId("hero-title").textContent = "Duriin cannot read its autonomy state.";
|
|
byId("hero-description").textContent = error.message;
|
|
byId("hypothesis-list").innerHTML = '<div class="empty-state">Runtime data is unavailable.</div>';
|
|
toast("Autonomy overview unavailable", true);
|
|
});
|
|
})();
|