mergen-server 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +57 -0
- package/README.md +96 -0
- package/dist/__stubs__/calibration.js +471 -0
- package/dist/__stubs__/causal.js +601 -0
- package/dist/__stubs__/closed-source.js +150 -0
- package/dist/__stubs__/license.js +35 -0
- package/dist/__stubs__/plans.js +197 -0
- package/dist/app.js +467 -0
- package/dist/cli.js +250 -0
- package/dist/commands/agent-identity.js +79 -0
- package/dist/commands/gate.js +107 -0
- package/dist/commands/github.js +632 -0
- package/dist/commands/incident.js +644 -0
- package/dist/commands/policy.js +710 -0
- package/dist/commands/scan.js +243 -0
- package/dist/commands/setup.js +1218 -0
- package/dist/commands/shared.js +109 -0
- package/dist/commands/team.js +809 -0
- package/dist/datadog/blame-attribution.js +228 -0
- package/dist/datadog/client.js +159 -0
- package/dist/datadog/compactor.js +169 -0
- package/dist/datadog/fingerprinter.js +34 -0
- package/dist/datadog/incident-state.js +21 -0
- package/dist/datadog/line-matcher.js +56 -0
- package/dist/datadog/memory-store.js +399 -0
- package/dist/datadog/otel-trace.js +67 -0
- package/dist/index.js +505 -0
- package/dist/intelligence/action-risk.js +132 -0
- package/dist/intelligence/activity-feed.js +40 -0
- package/dist/intelligence/agent-identity.js +115 -0
- package/dist/intelligence/agent-pipeline.js +329 -0
- package/dist/intelligence/agent-profiles.js +81 -0
- package/dist/intelligence/ai-commit.js +16 -0
- package/dist/intelligence/approval-analytics.js +42 -0
- package/dist/intelligence/approval-events.js +5 -0
- package/dist/intelligence/arch-boundaries.js +94 -0
- package/dist/intelligence/arch-graph.js +117 -0
- package/dist/intelligence/autonomy.js +397 -0
- package/dist/intelligence/azure-broker.js +57 -0
- package/dist/intelligence/azure-proxy-sessions.js +49 -0
- package/dist/intelligence/baseline.js +1 -0
- package/dist/intelligence/behavior-baseline.js +134 -0
- package/dist/intelligence/billing.js +1 -0
- package/dist/intelligence/blast-radius.js +224 -0
- package/dist/intelligence/bypass.js +157 -0
- package/dist/intelligence/calibration-classifier.js +77 -0
- package/dist/intelligence/calibration-git-sync.js +74 -0
- package/dist/intelligence/calibration.js +1 -0
- package/dist/intelligence/cascade-detector.js +58 -0
- package/dist/intelligence/case-study-generator.js +126 -0
- package/dist/intelligence/causal-graph.js +126 -0
- package/dist/intelligence/causal.js +1 -0
- package/dist/intelligence/change-risk.js +102 -0
- package/dist/intelligence/compliance-report.js +134 -0
- package/dist/intelligence/confidence-report.js +85 -0
- package/dist/intelligence/corpus-to-policy.js +140 -0
- package/dist/intelligence/credential-broker.js +112 -0
- package/dist/intelligence/credential-misuse.js +39 -0
- package/dist/intelligence/debug-sessions.js +108 -0
- package/dist/intelligence/decision-escalation.js +36 -0
- package/dist/intelligence/decision-memory.js +47 -0
- package/dist/intelligence/decision-similarity.js +85 -0
- package/dist/intelligence/default-safety-tests.js +53 -0
- package/dist/intelligence/degradation-watcher.js +108 -0
- package/dist/intelligence/detector-plugins.js +70 -0
- package/dist/intelligence/detectors.js +1 -0
- package/dist/intelligence/device-auth.js +138 -0
- package/dist/intelligence/diff-size.js +42 -0
- package/dist/intelligence/ebpf-verify.js +45 -0
- package/dist/intelligence/enterprise-policy-engine.js +1122 -0
- package/dist/intelligence/error-fingerprint.js +1 -0
- package/dist/intelligence/execution-gate.js +100 -0
- package/dist/intelligence/execution-mode.js +22 -0
- package/dist/intelligence/gate-analytics.js +256 -0
- package/dist/intelligence/gate-decision.js +64 -0
- package/dist/intelligence/gcp-broker.js +50 -0
- package/dist/intelligence/git-adr-sync.js +207 -0
- package/dist/intelligence/hitl-hold.js +460 -0
- package/dist/intelligence/hypothesis-history.js +93 -0
- package/dist/intelligence/idp-identity.js +83 -0
- package/dist/intelligence/impl-critic.js +216 -0
- package/dist/intelligence/incident-autopilot.js +622 -0
- package/dist/intelligence/incident-replay.js +162 -0
- package/dist/intelligence/incident-result-cache.js +33 -0
- package/dist/intelligence/incident-similarity.js +58 -0
- package/dist/intelligence/infra-detectors.js +312 -0
- package/dist/intelligence/license.js +185 -0
- package/dist/intelligence/llm-spokesperson.js +62 -0
- package/dist/intelligence/mcp-prompts.js +320 -0
- package/dist/intelligence/mcp-resources.js +171 -0
- package/dist/intelligence/normalize.js +108 -0
- package/dist/intelligence/notifications.js +83 -0
- package/dist/intelligence/oidc-issuer.js +87 -0
- package/dist/intelligence/override-corpus.js +597 -0
- package/dist/intelligence/planning-gate.js +84 -0
- package/dist/intelligence/plans.js +223 -0
- package/dist/intelligence/platt-scaling.js +164 -0
- package/dist/intelligence/policy-proposals.js +84 -0
- package/dist/intelligence/policy-suggester.js +161 -0
- package/dist/intelligence/policy-sync.js +103 -0
- package/dist/intelligence/policy-test-runner.js +35 -0
- package/dist/intelligence/postmortem-parser.js +181 -0
- package/dist/intelligence/postmortem-retrieval.js +224 -0
- package/dist/intelligence/postmortem-store.js +596 -0
- package/dist/intelligence/pr-commenter.js +81 -0
- package/dist/intelligence/pr-policy-gate.js +38 -0
- package/dist/intelligence/pr-shadow-analyzer.js +229 -0
- package/dist/intelligence/process-tree.js +108 -0
- package/dist/intelligence/prompts.js +1 -0
- package/dist/intelligence/replay.js +38 -0
- package/dist/intelligence/repro-steps.js +1 -0
- package/dist/intelligence/resource-extractors.js +0 -0
- package/dist/intelligence/resource-file-context.js +138 -0
- package/dist/intelligence/risk-score.js +22 -0
- package/dist/intelligence/rollback.js +137 -0
- package/dist/intelligence/runbook-updater.js +143 -0
- package/dist/intelligence/sandbox.js +194 -0
- package/dist/intelligence/script-trust.js +104 -0
- package/dist/intelligence/session-metrics.js +67 -0
- package/dist/intelligence/session-threat-tracker.js +231 -0
- package/dist/intelligence/shadow-digest-cron.js +81 -0
- package/dist/intelligence/shadow-log.js +284 -0
- package/dist/intelligence/shell-ast.js +145 -0
- package/dist/intelligence/siem-forward.js +104 -0
- package/dist/intelligence/slack-digest.js +151 -0
- package/dist/intelligence/slack-override-loop.js +217 -0
- package/dist/intelligence/slack-routing.js +67 -0
- package/dist/intelligence/slack.js +900 -0
- package/dist/intelligence/sql-ast.js +31 -0
- package/dist/intelligence/team.js +1 -0
- package/dist/intelligence/telemetry.js +76 -0
- package/dist/intelligence/threshold-optimizer.js +75 -0
- package/dist/intelligence/token-budget.js +66 -0
- package/dist/intelligence/tool-guard.js +708 -0
- package/dist/intelligence/tool-manifest 2.js +74 -0
- package/dist/intelligence/tool-manifest.js +125 -0
- package/dist/intelligence/tools-analysis.js +857 -0
- package/dist/intelligence/tools-arch.js +152 -0
- package/dist/intelligence/tools-autonomy.js +495 -0
- package/dist/intelligence/tools-blast-radius.js +140 -0
- package/dist/intelligence/tools-browser.js +431 -0
- package/dist/intelligence/tools-change-timeline.js +154 -0
- package/dist/intelligence/tools-credentials.js +117 -0
- package/dist/intelligence/tools-datadog.js +233 -0
- package/dist/intelligence/tools-debug-sessions.js +209 -0
- package/dist/intelligence/tools-discovery.js +284 -0
- package/dist/intelligence/tools-infra.js +612 -0
- package/dist/intelligence/tools-intent.js +128 -0
- package/dist/intelligence/tools-memory.js +514 -0
- package/dist/intelligence/tools-runbook.js +951 -0
- package/dist/intelligence/tools-sessions.js +107 -0
- package/dist/intelligence/tools-state.js +232 -0
- package/dist/intelligence/tools-utility.js +430 -0
- package/dist/intelligence/tools-validate.js +215 -0
- package/dist/intelligence/tools.js +1 -0
- package/dist/intelligence/trace-context.js +11 -0
- package/dist/intelligence/unclassified-clusters.js +87 -0
- package/dist/intelligence/usage.js +195 -0
- package/dist/routes/active-authors.js +63 -0
- package/dist/routes/activity-feed.js +21 -0
- package/dist/routes/adr.js +117 -0
- package/dist/routes/agent-activity.js +184 -0
- package/dist/routes/agent-blunders.js +163 -0
- package/dist/routes/agents.js +175 -0
- package/dist/routes/api-keys.js +65 -0
- package/dist/routes/arch.js +75 -0
- package/dist/routes/audit-export.js +93 -0
- package/dist/routes/billing-dashboard.js +158 -0
- package/dist/routes/billing-outcome.js +77 -0
- package/dist/routes/calibration.js +305 -0
- package/dist/routes/ci-gate-history.js +20 -0
- package/dist/routes/ci-gate.js +232 -0
- package/dist/routes/ci.js +293 -0
- package/dist/routes/compliance-report.js +24 -0
- package/dist/routes/confidence.js +30 -0
- package/dist/routes/credentials.js +119 -0
- package/dist/routes/dashboard.js +1407 -0
- package/dist/routes/decision-memory.js +42 -0
- package/dist/routes/demo.js +844 -0
- package/dist/routes/device-auth-stub.js +173 -0
- package/dist/routes/explain-why.js +39 -0
- package/dist/routes/gate-analytics.js +213 -0
- package/dist/routes/gate.js +28 -0
- package/dist/routes/github-webhook.js +364 -0
- package/dist/routes/habituation.js +30 -0
- package/dist/routes/health-integrations.js +243 -0
- package/dist/routes/heartbeats.js +45 -0
- package/dist/routes/hitl.js +364 -0
- package/dist/routes/impact-report.js +645 -0
- package/dist/routes/incident-webhook.js +63 -0
- package/dist/routes/incidents.js +366 -0
- package/dist/routes/layers.js +62 -0
- package/dist/routes/license.js +187 -0
- package/dist/routes/oidc-issuer.js +30 -0
- package/dist/routes/onboarding.js +170 -0
- package/dist/routes/otel.js +67 -0
- package/dist/routes/otlp-receiver.js +179 -0
- package/dist/routes/overrides.js +241 -0
- package/dist/routes/pagerduty.js +258 -0
- package/dist/routes/policies.js +809 -0
- package/dist/routes/policy-nl.js +169 -0
- package/dist/routes/postmortem.js +102 -0
- package/dist/routes/pr-shadow.js +47 -0
- package/dist/routes/rbac.js +61 -0
- package/dist/routes/replay.js +17 -0
- package/dist/routes/risk-report.js +164 -0
- package/dist/routes/runbooks.js +125 -0
- package/dist/routes/safety-test.js +174 -0
- package/dist/routes/sdk.js +222 -0
- package/dist/routes/sensor.js +819 -0
- package/dist/routes/sentry.js +140 -0
- package/dist/routes/sessions.js +43 -0
- package/dist/routes/setup-ui.js +594 -0
- package/dist/routes/shadow-report.js +107 -0
- package/dist/routes/slack-routing.js +171 -0
- package/dist/routes/team-usage.js +71 -0
- package/dist/routes/telemetry.js +28 -0
- package/dist/routes/tenants.js +199 -0
- package/dist/routes/tickets.js +167 -0
- package/dist/routes/validate.js +13 -0
- package/dist/routes/war-room.js +113 -0
- package/dist/scripts/check-arch.js +22 -0
- package/dist/seeds/community-corpus.js +176 -0
- package/dist/seeds/corpus.js +1224 -0
- package/dist/sensor/action-ledger.js +327 -0
- package/dist/sensor/adr-store.js +140 -0
- package/dist/sensor/agent-blunder-store.js +370 -0
- package/dist/sensor/agent-context-store.js +225 -0
- package/dist/sensor/agent-memory-store.js +231 -0
- package/dist/sensor/audit-fetch.js +23 -0
- package/dist/sensor/audit-log.js +273 -0
- package/dist/sensor/buffer-schemas.js +225 -0
- package/dist/sensor/buffer.js +599 -0
- package/dist/sensor/bypass-tracker.js +93 -0
- package/dist/sensor/ci-gate-history.js +70 -0
- package/dist/sensor/cloud-auth.js +183 -0
- package/dist/sensor/commit-context-store.js +235 -0
- package/dist/sensor/docker-log-stream.js +164 -0
- package/dist/sensor/docker-monitor.js +122 -0
- package/dist/sensor/extended-buffer.js +46 -0
- package/dist/sensor/feedback-token.js +41 -0
- package/dist/sensor/file-lock.js +6 -0
- package/dist/sensor/fs-watcher.js +106 -0
- package/dist/sensor/gate-heartbeat.js +122 -0
- package/dist/sensor/git-suspect.js +136 -0
- package/dist/sensor/habituation-store.js +77 -0
- package/dist/sensor/heartbeat-monitor.js +133 -0
- package/dist/sensor/incident-store.js +340 -0
- package/dist/sensor/infra-normalizer.js +513 -0
- package/dist/sensor/ingest.js +339 -0
- package/dist/sensor/jest-reporter.js +52 -0
- package/dist/sensor/k8s-events.js +132 -0
- package/dist/sensor/layer2-store.js +111 -0
- package/dist/sensor/layer3-store.js +230 -0
- package/dist/sensor/layer4-store.js +147 -0
- package/dist/sensor/logger.js +13 -0
- package/dist/sensor/otel-exporter.js +161 -0
- package/dist/sensor/paths.js +71 -0
- package/dist/sensor/policy-history.js +147 -0
- package/dist/sensor/pr-shadow-store.js +83 -0
- package/dist/sensor/process-watcher.js +162 -0
- package/dist/sensor/rbac.js +92 -0
- package/dist/sensor/redact.js +114 -0
- package/dist/sensor/redis-store.js +191 -0
- package/dist/sensor/route-reachability.js +64 -0
- package/dist/sensor/security-utils.js +18 -0
- package/dist/sensor/service-graph.js +170 -0
- package/dist/sensor/service-topology.js +227 -0
- package/dist/sensor/session-history.js +74 -0
- package/dist/sensor/session-persist.js +38 -0
- package/dist/sensor/shadow-promote.js +122 -0
- package/dist/sensor/sourcemap.js +281 -0
- package/dist/sensor/sqlite-store.js +205 -0
- package/dist/sensor/sso.js +164 -0
- package/dist/sensor/vitest-reporter.js +65 -0
- package/dist/sensor/watcher.js +48 -0
- package/dist/storage/interfaces.js +0 -0
- package/dist/storage/pg/pg-action-ledger.js +138 -0
- package/dist/storage/pg/pg-approval-store.js +107 -0
- package/dist/storage/pg/pg-blunder-store.js +193 -0
- package/dist/storage/pg/pg-ci-gate-history.js +83 -0
- package/dist/storage/pg/pg-client.js +24 -0
- package/dist/storage/pg/pg-event-store.js +63 -0
- package/dist/storage/pg/pg-incident-store.js +190 -0
- package/dist/storage/pg/pg-migrations.js +26 -0
- package/dist/storage/pg/pg-override-corpus.js +447 -0
- package/dist/storage/pg/pg-shadow-log.js +129 -0
- package/dist/storage/sqlite/sqlite-action-ledger.js +18 -0
- package/dist/storage/sqlite/sqlite-approval-store.js +37 -0
- package/dist/storage/sqlite/sqlite-blunder-store.js +27 -0
- package/dist/storage/sqlite/sqlite-ci-gate-history.js +19 -0
- package/dist/storage/sqlite/sqlite-event-store.js +29 -0
- package/dist/storage/sqlite/sqlite-incident-store.js +34 -0
- package/dist/storage/sqlite/sqlite-override-corpus.js +75 -0
- package/dist/storage/sqlite/sqlite-shadow-log.js +36 -0
- package/dist/storage/store-factory.js +55 -0
- package/dist/storage/store-registry.js +31 -0
- package/dist/update-checker.js +107 -0
- package/dist/workers/autopilot-worker.js +28 -0
- package/dist/workers/notification-worker.js +28 -0
- package/dist/workers/queues.js +58 -0
- package/dist/workers/validation-worker.js +24 -0
- package/dist/workers/worker-registry.js +18 -0
- package/package.json +123 -0
- package/sdk/mergen-inject.js +260 -0
- package/sdk/node.js +313 -0
- package/sdk/vite-plugin.ts +57 -0
- package/sdk/webpack-plugin.js +106 -0
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { Router } from "express";
|
|
3
|
+
import { getShadowLog } from "../intelligence/shadow-log.js";
|
|
4
|
+
import { getStores } from "../storage/store-registry.js";
|
|
5
|
+
import { postmortemStore } from "../intelligence/postmortem-store.js";
|
|
6
|
+
import { plattScale } from "../intelligence/platt-scaling.js";
|
|
7
|
+
import logger from "../sensor/logger.js";
|
|
8
|
+
const DEFAULT_REVENUE_PER_MINUTE_USD = 100;
|
|
9
|
+
const DEFAULT_DOWNTIME_COST_PER_HOUR_USD = 1e4;
|
|
10
|
+
const ESTIMATED_AUTONOMOUS_MTTR_MS = 2 * 60 * 1e3;
|
|
11
|
+
function createImpactReportRouter() {
|
|
12
|
+
const router = Router();
|
|
13
|
+
router.get("/impact-report", async (req, res) => {
|
|
14
|
+
const windowDays = Math.min(90, Math.max(1, Number(req.query.days ?? 30)));
|
|
15
|
+
const format = req.query.format;
|
|
16
|
+
const data = await computeImpactData(windowDays, req.tenantId);
|
|
17
|
+
logger.info({ windowDays, format }, "impact-report: generated");
|
|
18
|
+
if (format === "html") {
|
|
19
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
20
|
+
res.setHeader("Content-Security-Policy", "default-src 'self'; style-src 'unsafe-inline'");
|
|
21
|
+
res.send(buildHtml(data));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (format === "slide") {
|
|
25
|
+
res.json({ ok: true, slide: buildSlide(data) });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
res.json({ ok: true, report: data });
|
|
29
|
+
});
|
|
30
|
+
return router;
|
|
31
|
+
}
|
|
32
|
+
function fmtMs(ms) {
|
|
33
|
+
if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
|
|
34
|
+
const min = Math.floor(ms / 6e4);
|
|
35
|
+
const sec = Math.round(ms % 6e4 / 1e3);
|
|
36
|
+
return sec > 0 ? `${min}m ${sec}s` : `${min}m`;
|
|
37
|
+
}
|
|
38
|
+
async function computeImpactData(windowDays, tenantId) {
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
const cutoff = now - windowDays * 24 * 60 * 60 * 1e3;
|
|
41
|
+
const entries = [...getShadowLog()].filter((e) => e.recordedAt >= cutoff);
|
|
42
|
+
const wouldResolve = entries.filter((e) => e.wouldHaveExecuted);
|
|
43
|
+
const missed = entries.filter((e) => !e.wouldHaveExecuted);
|
|
44
|
+
const incidentMap = new Map(
|
|
45
|
+
(await getStores().incidents.list(void 0, 1e4, tenantId)).map((i) => [i.pid, i])
|
|
46
|
+
);
|
|
47
|
+
const corpus = await getStores().overrides.getOverrideSummary(tenantId);
|
|
48
|
+
const overrideIds = [...new Set(entries.map((e) => e.overrideId).filter((id) => !!id))];
|
|
49
|
+
const overrideDetails = new Map(
|
|
50
|
+
await Promise.all(overrideIds.map(async (id) => [id, await getStores().overrides.getOverrideById(id, tenantId)]))
|
|
51
|
+
);
|
|
52
|
+
const autonomousMttrSamples = [];
|
|
53
|
+
const manualMttrSamples = [];
|
|
54
|
+
const contextAssistedMttrSamples = [];
|
|
55
|
+
const unassistedMttrSamples = [];
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
if (!entry.pid) continue;
|
|
58
|
+
const inc = incidentMap.get(entry.pid);
|
|
59
|
+
if (!inc?.resolvedAt || !inc.createdAt) continue;
|
|
60
|
+
const mttr = inc.resolvedAt - inc.createdAt;
|
|
61
|
+
if (inc.resolvedAutonomously) {
|
|
62
|
+
autonomousMttrSamples.push(mttr);
|
|
63
|
+
} else {
|
|
64
|
+
manualMttrSamples.push(mttr);
|
|
65
|
+
if (inc.contextBriefViewedAt != null) {
|
|
66
|
+
contextAssistedMttrSamples.push(mttr);
|
|
67
|
+
} else {
|
|
68
|
+
unassistedMttrSamples.push(mttr);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const allMttrSamples = [...autonomousMttrSamples, ...manualMttrSamples];
|
|
73
|
+
const avg = (arr) => arr.length > 0 ? arr.reduce((a, b) => a + b, 0) / arr.length : null;
|
|
74
|
+
const avgAutonomousMttrMs = avg(autonomousMttrSamples);
|
|
75
|
+
const avgManualMttrMs = avg(manualMttrSamples);
|
|
76
|
+
const avgActualMttrMs = avg(allMttrSamples);
|
|
77
|
+
const avgContextAssistedMttrMs = avg(contextAssistedMttrSamples);
|
|
78
|
+
const avgUnassistedMttrMs = avg(unassistedMttrSamples);
|
|
79
|
+
const autonomousBenchmark = avgAutonomousMttrMs ?? ESTIMATED_AUTONOMOUS_MTTR_MS;
|
|
80
|
+
const manualBenchmark = avgManualMttrMs ?? avgActualMttrMs;
|
|
81
|
+
const mttrReductionPct = manualBenchmark !== null && manualBenchmark > 0 ? Math.round((1 - autonomousBenchmark / manualBenchmark) * 100) : null;
|
|
82
|
+
const highConfidence = entries.filter((e) => e.remediationConfidence >= 0.85).length;
|
|
83
|
+
const mediumConfidence = entries.filter(
|
|
84
|
+
(e) => e.remediationConfidence >= 0.7 && e.remediationConfidence < 0.85
|
|
85
|
+
).length;
|
|
86
|
+
const lowOrNoCommand = entries.filter(
|
|
87
|
+
(e) => e.remediationConfidence < 0.7 || e.skipReason === "no-command"
|
|
88
|
+
).length;
|
|
89
|
+
const tagMap = /* @__PURE__ */ new Map();
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
const t = tagMap.get(entry.incidentTag) ?? { total: 0, wouldResolve: 0, mttrSamples: [] };
|
|
92
|
+
t.total += 1;
|
|
93
|
+
if (entry.wouldHaveExecuted) t.wouldResolve += 1;
|
|
94
|
+
if (entry.pid) {
|
|
95
|
+
const inc = incidentMap.get(entry.pid);
|
|
96
|
+
if (inc?.resolvedAt && inc.createdAt) t.mttrSamples.push(inc.resolvedAt - inc.createdAt);
|
|
97
|
+
}
|
|
98
|
+
tagMap.set(entry.incidentTag, t);
|
|
99
|
+
}
|
|
100
|
+
const byTag = [...tagMap.entries()].map(([tag, t]) => ({
|
|
101
|
+
tag,
|
|
102
|
+
total: t.total,
|
|
103
|
+
wouldResolve: t.wouldResolve,
|
|
104
|
+
resolutionRate: t.total > 0 ? t.wouldResolve / t.total : 0,
|
|
105
|
+
avgActualMttrMs: t.mttrSamples.length > 0 ? t.mttrSamples.reduce((a, b) => a + b, 0) / t.mttrSamples.length : null
|
|
106
|
+
})).sort((a, b) => b.total - a.total);
|
|
107
|
+
const corpusBlockCount = entries.filter((e) => e.skipReason === "override-corpus").length;
|
|
108
|
+
const reviewed = entries.filter((e) => e.humanVerdict !== void 0);
|
|
109
|
+
const approved = reviewed.filter((e) => e.humanVerdict === "would-approve").length;
|
|
110
|
+
const humanApprovalRate = reviewed.length >= 3 ? approved / reviewed.length : null;
|
|
111
|
+
const rawHighConfidenceEntries = entries.filter((e) => e.remediationConfidence >= 0.85 && e.humanVerdict !== void 0);
|
|
112
|
+
const rawHighConfidenceApproved = rawHighConfidenceEntries.filter((e) => e.humanVerdict === "would-approve").length;
|
|
113
|
+
const rawHighConfidenceApprovedRate = rawHighConfidenceEntries.length > 0 ? rawHighConfidenceApproved / rawHighConfidenceEntries.length : null;
|
|
114
|
+
const plattHighConfidenceEntries = entries.filter((e) => {
|
|
115
|
+
const cal = plattScale(e.remediationConfidence, e.incidentTag).calibrated;
|
|
116
|
+
return cal >= 0.85 && e.humanVerdict !== void 0;
|
|
117
|
+
});
|
|
118
|
+
const plattHighConfidenceApproved = plattHighConfidenceEntries.filter((e) => e.humanVerdict === "would-approve").length;
|
|
119
|
+
const plattHighConfidenceApprovedRate = plattHighConfidenceEntries.length > 0 ? plattHighConfidenceApproved / plattHighConfidenceEntries.length : null;
|
|
120
|
+
const overrodeCount = reviewed.filter((e) => e.humanVerdict === "would-override").length;
|
|
121
|
+
const falsePositiveRate = reviewed.length > 0 ? overrodeCount / reviewed.length : null;
|
|
122
|
+
const falsePositiveCount = overrodeCount;
|
|
123
|
+
const executionBlocks = entries.filter((e) => e.remediationConfidence >= 0.85 && e.skipReason !== "executed" && e.skipReason !== "executed-failure").map((e) => {
|
|
124
|
+
let blockReason = "Safety block";
|
|
125
|
+
if (e.skipReason === "override-corpus") blockReason = "override corpus block";
|
|
126
|
+
else if (e.skipReason === "similar-incident-history") blockReason = "historical decision memory hold";
|
|
127
|
+
else if (e.skipReason === "planning-gate") blockReason = "planning gate block";
|
|
128
|
+
else if (e.skipReason === "level-restricted") blockReason = "restricted by autopilot level";
|
|
129
|
+
else if (e.skipReason === "denied") blockReason = "denied via Slack";
|
|
130
|
+
else if (e.skipReason === "blocked-by-safety-filter") blockReason = "blocked by safety filter";
|
|
131
|
+
else if (e.skipReason === "track-record-pause") blockReason = "paused due to wrong verdicts";
|
|
132
|
+
return {
|
|
133
|
+
pid: e.pid,
|
|
134
|
+
date: new Date(e.firedAt ?? e.recordedAt).toISOString().slice(0, 16).replace("T", " ") + " UTC",
|
|
135
|
+
service: e.service,
|
|
136
|
+
command: e.command ?? "unknown",
|
|
137
|
+
confidence: e.remediationConfidence,
|
|
138
|
+
blockedBy: e.skipReason,
|
|
139
|
+
reason: blockReason
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
const revenuePerMinute = parseFloat(process.env.MERGEN_REVENUE_PER_MINUTE_USD ?? "") || DEFAULT_REVENUE_PER_MINUTE_USD;
|
|
143
|
+
const autonomousBenchmarkForRevenue = avgAutonomousMttrMs ?? ESTIMATED_AUTONOMOUS_MTTR_MS;
|
|
144
|
+
const manualBenchmarkForRevenue = avgManualMttrMs ?? avgActualMttrMs;
|
|
145
|
+
const mttrSavedMs = manualBenchmarkForRevenue != null ? Math.max(0, manualBenchmarkForRevenue - autonomousBenchmarkForRevenue) : null;
|
|
146
|
+
const estimatedRevenuePreservedUsd = mttrSavedMs != null && wouldResolve.length > 0 ? Math.round(mttrSavedMs / 6e4 * revenuePerMinute * wouldResolve.length) : null;
|
|
147
|
+
const downtimeCostPerHour = parseFloat(process.env.MERGEN_DOWNTIME_COST_PER_HOUR ?? "") || DEFAULT_DOWNTIME_COST_PER_HOUR_USD;
|
|
148
|
+
const estimatedDowntimeSavedUsd = mttrSavedMs != null && wouldResolve.length > 0 ? Math.round(mttrSavedMs / 36e5 * downtimeCostPerHour * wouldResolve.length) : null;
|
|
149
|
+
const corpusPostmortems = postmortemStore.count();
|
|
150
|
+
const [blunderStats, chainResult] = await Promise.all([
|
|
151
|
+
getStores().blunders.getStats(tenantId),
|
|
152
|
+
getStores().blunders.verifyChain(tenantId)
|
|
153
|
+
]);
|
|
154
|
+
const agentBlunderSummary = {
|
|
155
|
+
totalPrevented: blunderStats.total,
|
|
156
|
+
byType: blunderStats.byType,
|
|
157
|
+
chainVerified: chainResult.valid
|
|
158
|
+
};
|
|
159
|
+
const savedPerIncidentMs = Math.max(
|
|
160
|
+
0,
|
|
161
|
+
(manualBenchmarkForRevenue ?? 0) - autonomousBenchmarkForRevenue
|
|
162
|
+
);
|
|
163
|
+
const timeSavedHours = savedPerIncidentMs > 0 && wouldResolve.length > 0 ? Math.round(savedPerIncidentMs * wouldResolve.length / 36e5 * 10) / 10 : null;
|
|
164
|
+
const hoursPerIncident = savedPerIncidentMs > 0 ? Math.round(savedPerIncidentMs / 36e5 * 10) / 10 : null;
|
|
165
|
+
const timeSavedLabel = timeSavedHours != null && wouldResolve.length > 0 ? `${timeSavedHours}h saved across ${wouldResolve.length} incident${wouldResolve.length !== 1 ? "s" : ""}` : null;
|
|
166
|
+
const rate = entries.length > 0 ? Math.round(wouldResolve.length / entries.length * 100) : 0;
|
|
167
|
+
const topTags = byTag.slice(0, 3).map((t) => `${t.tag.replace(/^infra_/, "")}\xD7${t.total}`).join(", ");
|
|
168
|
+
const tagLine = topTags ? ` Top failure modes: ${topTags}.` : "";
|
|
169
|
+
let mttrLine = "";
|
|
170
|
+
if (avgAutonomousMttrMs !== null && avgManualMttrMs !== null) {
|
|
171
|
+
mttrLine = ` MTTR: ${fmtMs(avgAutonomousMttrMs)} autonomous (n=${autonomousMttrSamples.length}) vs. ${fmtMs(avgManualMttrMs)} manual (n=${manualMttrSamples.length}).`;
|
|
172
|
+
} else if (avgManualMttrMs !== null) {
|
|
173
|
+
mttrLine = ` Est. autonomous MTTR: ${fmtMs(ESTIMATED_AUTONOMOUS_MTTR_MS)} vs. ${fmtMs(avgManualMttrMs)} manual (n=${manualMttrSamples.length}).`;
|
|
174
|
+
} else if (avgActualMttrMs !== null) {
|
|
175
|
+
mttrLine = ` Average time-to-fix: ${fmtMs(ESTIMATED_AUTONOMOUS_MTTR_MS)} est. autonomous vs. ${fmtMs(avgActualMttrMs)} observed (n=${allMttrSamples.length}).`;
|
|
176
|
+
}
|
|
177
|
+
let contextAssistedLine = "";
|
|
178
|
+
if (avgContextAssistedMttrMs !== null && avgUnassistedMttrMs !== null) {
|
|
179
|
+
contextAssistedLine = ` Context-assisted manual MTTR: ${fmtMs(avgContextAssistedMttrMs)} (n=${contextAssistedMttrSamples.length}) vs. ${fmtMs(avgUnassistedMttrMs)} without (n=${unassistedMttrSamples.length}).`;
|
|
180
|
+
}
|
|
181
|
+
const timeSavedSentence = timeSavedLabel ? ` ${timeSavedLabel} of engineer time.` : "";
|
|
182
|
+
const _reviewedCount = reviewed.length;
|
|
183
|
+
const _corpusSize = corpus.length;
|
|
184
|
+
let falsePositiveLine = "";
|
|
185
|
+
if (falsePositiveRate !== null && _reviewedCount >= 5) {
|
|
186
|
+
const fpPct = Math.round(falsePositiveRate * 100);
|
|
187
|
+
falsePositiveLine = ` False positive rate: ${fpPct}% (${falsePositiveCount} of ${_reviewedCount} human-reviewed diagnoses).`;
|
|
188
|
+
} else if (_reviewedCount > 0 && _reviewedCount < 5) {
|
|
189
|
+
falsePositiveLine = ` False positive rate: pending (${_reviewedCount} of 5 human reviews needed for a reliable estimate).`;
|
|
190
|
+
}
|
|
191
|
+
let overrideFrequencyLine = "";
|
|
192
|
+
if (corpusBlockCount > 0) {
|
|
193
|
+
overrideFrequencyLine = ` Override corpus blocked ${corpusBlockCount} autonomous action${corpusBlockCount !== 1 ? "s" : ""} across ${_corpusSize} encoded pattern${_corpusSize !== 1 ? "s" : ""}.`;
|
|
194
|
+
} else if (_corpusSize > 0) {
|
|
195
|
+
overrideFrequencyLine = ` Override corpus has ${_corpusSize} encoded pattern${_corpusSize !== 1 ? "s" : ""} \u2014 none triggered in this window.`;
|
|
196
|
+
}
|
|
197
|
+
const deckSummary = `Mergen processed ${entries.length} incident${entries.length !== 1 ? "s" : ""} (n=${entries.length}).${tagLine} Autonomous resolution would have applied correctly ${wouldResolve.length} time${wouldResolve.length !== 1 ? "s" : ""} (${rate}%).` + mttrLine + contextAssistedLine + timeSavedSentence + falsePositiveLine + overrideFrequencyLine;
|
|
198
|
+
const comparisonRows = entries.slice().sort((a, b) => (b.firedAt ?? b.recordedAt) - (a.firedAt ?? a.recordedAt)).map((entry) => {
|
|
199
|
+
const inc = entry.pid ? incidentMap.get(entry.pid) : null;
|
|
200
|
+
const actualMttrMs = inc?.resolvedAt && inc.createdAt ? inc.resolvedAt - inc.createdAt : null;
|
|
201
|
+
let engineerAction = "Not yet reviewed";
|
|
202
|
+
let agreementType = "unreviewed";
|
|
203
|
+
let overrideReason = null;
|
|
204
|
+
if (entry.skipReason === "executed") {
|
|
205
|
+
engineerAction = "Executed autonomously";
|
|
206
|
+
agreementType = "agree";
|
|
207
|
+
} else if (entry.skipReason === "executed-failure") {
|
|
208
|
+
engineerAction = "Executed autonomously (failed)";
|
|
209
|
+
agreementType = "override";
|
|
210
|
+
overrideReason = "Command failed with non-zero exit code";
|
|
211
|
+
} else if (entry.skipReason === "blocked-by-safety-filter") {
|
|
212
|
+
engineerAction = "Blocked by safety gate";
|
|
213
|
+
agreementType = "override";
|
|
214
|
+
overrideReason = "Command triggered safety pattern blocklist";
|
|
215
|
+
} else if (entry.skipReason === "denied") {
|
|
216
|
+
engineerAction = "Denied by engineer";
|
|
217
|
+
agreementType = "override";
|
|
218
|
+
overrideReason = "Manual override via Slack gate";
|
|
219
|
+
} else if (entry.humanVerdict === "would-approve") {
|
|
220
|
+
engineerAction = "Would apply same fix";
|
|
221
|
+
agreementType = "agree";
|
|
222
|
+
} else if (entry.humanVerdict === "would-override" && entry.overrideId) {
|
|
223
|
+
const ov = overrideDetails.get(entry.overrideId) ?? null;
|
|
224
|
+
engineerAction = ov?.manualAction ?? "Override \u2014 see corpus";
|
|
225
|
+
overrideReason = ov?.overrideReason ?? null;
|
|
226
|
+
agreementType = "override";
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
date: new Date(entry.firedAt ?? entry.recordedAt).toISOString().slice(0, 16).replace("T", " ") + " UTC",
|
|
230
|
+
service: entry.service,
|
|
231
|
+
failureMode: entry.incidentTag.replace(/^infra_/, ""),
|
|
232
|
+
mergenCommand: entry.command,
|
|
233
|
+
mergenConfidence: entry.remediationConfidence,
|
|
234
|
+
engineerAction,
|
|
235
|
+
agreementType,
|
|
236
|
+
overrideReason,
|
|
237
|
+
actualMttrMs,
|
|
238
|
+
outcome: inc?.status ?? null
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
const windowStartDate = new Date(cutoff);
|
|
242
|
+
const windowEndDate = new Date(now);
|
|
243
|
+
return {
|
|
244
|
+
generatedAt: new Date(now).toISOString(),
|
|
245
|
+
windowDays,
|
|
246
|
+
windowStart: windowStartDate.toISOString().slice(0, 10),
|
|
247
|
+
windowEnd: windowEndDate.toISOString().slice(0, 10),
|
|
248
|
+
totalIncidents: entries.length,
|
|
249
|
+
wouldResolveCount: wouldResolve.length,
|
|
250
|
+
wouldResolveRate: entries.length > 0 ? wouldResolve.length / entries.length : 0,
|
|
251
|
+
missedCount: missed.length,
|
|
252
|
+
estimatedAutonomousMttrMs: ESTIMATED_AUTONOMOUS_MTTR_MS,
|
|
253
|
+
avgAutonomousMttrMs,
|
|
254
|
+
autonomousMttrSampleSize: autonomousMttrSamples.length,
|
|
255
|
+
avgManualMttrMs,
|
|
256
|
+
manualMttrSampleSize: manualMttrSamples.length,
|
|
257
|
+
avgActualMttrMs,
|
|
258
|
+
mttrReductionPct,
|
|
259
|
+
mttrSelectionBiasCaveat: "Autonomous MTTR only covers incidents where confidence was \u226585% \u2014 the simpler, well-understood failure modes autopilot picked up. Manual MTTR covers all incidents, including complex ones autopilot skipped. For an unbiased comparison, filter manual MTTR to the same high-confidence cohort.",
|
|
260
|
+
avgContextAssistedMttrMs,
|
|
261
|
+
contextAssistedMttrSampleSize: contextAssistedMttrSamples.length,
|
|
262
|
+
avgUnassistedMttrMs,
|
|
263
|
+
unassistedMttrSampleSize: unassistedMttrSamples.length,
|
|
264
|
+
highConfidence,
|
|
265
|
+
mediumConfidence,
|
|
266
|
+
lowOrNoCommand,
|
|
267
|
+
overridePatterns: corpus.length,
|
|
268
|
+
corpusBlockCount,
|
|
269
|
+
byTag,
|
|
270
|
+
humanReviewedCount: reviewed.length,
|
|
271
|
+
humanApprovalRate,
|
|
272
|
+
rawHighConfidenceApprovedRate,
|
|
273
|
+
plattHighConfidenceApprovedRate,
|
|
274
|
+
falsePositiveRate,
|
|
275
|
+
falsePositiveCount,
|
|
276
|
+
executionBlocks,
|
|
277
|
+
comparisonRows,
|
|
278
|
+
deckSummary,
|
|
279
|
+
estimatedRevenuePreservedUsd,
|
|
280
|
+
estimatedDowntimeSavedUsd,
|
|
281
|
+
corpusPostmortems,
|
|
282
|
+
timeSavedHours,
|
|
283
|
+
hoursPerIncident,
|
|
284
|
+
timeSavedLabel,
|
|
285
|
+
agentBlunderSummary
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
function buildSlide(d) {
|
|
289
|
+
const msToMin = (ms) => ms == null ? null : Math.round(ms / 6e4 * 10) / 10;
|
|
290
|
+
const resolutionRate = d.totalIncidents > 0 ? Math.round(d.wouldResolveCount / d.totalIncidents * 100) : null;
|
|
291
|
+
return {
|
|
292
|
+
generatedAt: d.generatedAt,
|
|
293
|
+
windowDays: d.windowDays,
|
|
294
|
+
incidents_processed: d.totalIncidents,
|
|
295
|
+
autonomous_resolution_rate_pct: resolutionRate,
|
|
296
|
+
autonomous_resolution_count: d.wouldResolveCount,
|
|
297
|
+
mttr_autonomous_minutes: msToMin(d.avgAutonomousMttrMs ?? d.estimatedAutonomousMttrMs),
|
|
298
|
+
mttr_manual_minutes: msToMin(d.avgManualMttrMs),
|
|
299
|
+
mttr_reduction_pct: d.mttrReductionPct,
|
|
300
|
+
gate_false_positive_rate_pct: d.falsePositiveRate != null ? Math.round(d.falsePositiveRate * 100) : null,
|
|
301
|
+
gate_fp_annotation_coverage_pct: Math.round(
|
|
302
|
+
d.humanReviewedCount / Math.max(1, d.totalIncidents) * 100
|
|
303
|
+
),
|
|
304
|
+
agent_blunders_blocked: d.agentBlunderSummary.totalPrevented,
|
|
305
|
+
blunder_chain_verified: d.agentBlunderSummary.chainVerified,
|
|
306
|
+
deck_summary: d.deckSummary
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function pct(n) {
|
|
310
|
+
return `${Math.round(n * 100)}%`;
|
|
311
|
+
}
|
|
312
|
+
function num(n) {
|
|
313
|
+
return n.toLocaleString();
|
|
314
|
+
}
|
|
315
|
+
function buildHtml(d) {
|
|
316
|
+
const nonce = randomUUID().replace(/-/g, "");
|
|
317
|
+
const autoMttrLabel = d.avgAutonomousMttrMs !== null ? `${fmtMs(d.avgAutonomousMttrMs)} <span class="muted" style="font-weight:400;font-size:11px">(n=${d.autonomousMttrSampleSize} actual)</span>` : `${fmtMs(d.estimatedAutonomousMttrMs)} <span class="muted" style="font-weight:400;font-size:11px">(estimated)</span>`;
|
|
318
|
+
const manualMttr = d.avgManualMttrMs ?? d.avgActualMttrMs;
|
|
319
|
+
const manualN = d.avgManualMttrMs !== null ? d.manualMttrSampleSize : d.manualMttrSampleSize + d.autonomousMttrSampleSize;
|
|
320
|
+
const mttrRow = manualMttr !== null ? `
|
|
321
|
+
<tr>
|
|
322
|
+
<td>Avg. MTTR \u2014 manual resolution</td>
|
|
323
|
+
<td class="val">${fmtMs(manualMttr)} <span class="muted" style="font-weight:400;font-size:11px">(n=${manualN})</span></td>
|
|
324
|
+
</tr>
|
|
325
|
+
<tr>
|
|
326
|
+
<td>Avg. MTTR \u2014 autonomous resolution</td>
|
|
327
|
+
<td class="val green">${autoMttrLabel}</td>
|
|
328
|
+
</tr>
|
|
329
|
+
<tr>
|
|
330
|
+
<td>MTTR reduction</td>
|
|
331
|
+
<td class="val green">${d.mttrReductionPct !== null ? d.mttrReductionPct + "%" : "\u2014"}</td>
|
|
332
|
+
</tr>` : `
|
|
333
|
+
<tr>
|
|
334
|
+
<td>Autonomous MTTR</td>
|
|
335
|
+
<td class="val green">${autoMttrLabel}</td>
|
|
336
|
+
</tr>
|
|
337
|
+
<tr>
|
|
338
|
+
<td>Manual MTTR</td>
|
|
339
|
+
<td class="val muted">No resolved incidents in window</td>
|
|
340
|
+
</tr>`;
|
|
341
|
+
const tagRows = d.byTag.slice(0, 8).map((t) => `
|
|
342
|
+
<tr>
|
|
343
|
+
<td class="mono">${t.tag.replace(/^infra_/, "")}</td>
|
|
344
|
+
<td>${num(t.total)}</td>
|
|
345
|
+
<td>${num(t.wouldResolve)}</td>
|
|
346
|
+
<td class="${t.resolutionRate >= 0.85 ? "green" : t.resolutionRate >= 0.6 ? "yellow" : "red"}">${pct(t.resolutionRate)}</td>
|
|
347
|
+
<td class="muted">${t.avgActualMttrMs !== null ? fmtMs(t.avgActualMttrMs) : "\u2014"}</td>
|
|
348
|
+
</tr>`).join("");
|
|
349
|
+
const humanRow = d.humanApprovalRate !== null ? `<tr><td>Human approval rate (shadow review)</td><td class="val green">${pct(d.humanApprovalRate)} (${num(d.humanReviewedCount)} reviewed)</td></tr>` : `<tr><td>Human approval rate</td><td class="val muted">${d.humanReviewedCount < 3 ? "Awaiting reviews" : "\u2014"}</td></tr>`;
|
|
350
|
+
const overrideRow = d.overridePatterns > 0 ? `<tr><td>Override patterns learned</td><td class="val">${num(d.overridePatterns)} pattern${d.overridePatterns !== 1 ? "s" : ""} (${num(d.corpusBlockCount)} block${d.corpusBlockCount !== 1 ? "s" : ""})</td></tr>` : "";
|
|
351
|
+
const contextAssistedRow = d.avgContextAssistedMttrMs !== null && d.avgUnassistedMttrMs !== null ? `<tr>
|
|
352
|
+
<td>Manual MTTR \u2014 context-assisted (read brief)</td>
|
|
353
|
+
<td class="val green">${fmtMs(d.avgContextAssistedMttrMs)} <span class="muted" style="font-weight:400;font-size:11px">(n=${d.contextAssistedMttrSampleSize})</span></td>
|
|
354
|
+
</tr>
|
|
355
|
+
<tr>
|
|
356
|
+
<td>Manual MTTR \u2014 unassisted</td>
|
|
357
|
+
<td class="val">${fmtMs(d.avgUnassistedMttrMs)} <span class="muted" style="font-weight:400;font-size:11px">(n=${d.unassistedMttrSampleSize})</span></td>
|
|
358
|
+
</tr>` : "";
|
|
359
|
+
return `<!DOCTYPE html>
|
|
360
|
+
<html lang="en">
|
|
361
|
+
<head>
|
|
362
|
+
<meta charset="UTF-8">
|
|
363
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
364
|
+
<title>Mergen Impact Report \u2014 ${d.windowStart} to ${d.windowEnd}</title>
|
|
365
|
+
<style nonce="${nonce}">
|
|
366
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
367
|
+
:root{
|
|
368
|
+
--bg:#0f1117;--surface:#1a1d26;--border:#2a2d3a;
|
|
369
|
+
--text:#e2e8f0;--muted:#64748b;
|
|
370
|
+
--green:#22c55e;--yellow:#f59e0b;--red:#ef4444;--blue:#3b82f6;
|
|
371
|
+
--font:system-ui,-apple-system,sans-serif;
|
|
372
|
+
}
|
|
373
|
+
@media print{
|
|
374
|
+
:root{--bg:#fff;--surface:#f8f9fa;--border:#dee2e6;--text:#212529;--muted:#6c757d;
|
|
375
|
+
--green:#198754;--yellow:#fd7e14;--red:#dc3545;}
|
|
376
|
+
body{font-size:11pt}
|
|
377
|
+
.no-print{display:none}
|
|
378
|
+
a{color:var(--text)}
|
|
379
|
+
}
|
|
380
|
+
body{background:var(--bg);color:var(--text);font-family:var(--font);font-size:13px;line-height:1.6;padding:40px 24px;max-width:860px;margin:0 auto}
|
|
381
|
+
h1{font-size:22px;font-weight:700;margin-bottom:4px}
|
|
382
|
+
.subtitle{color:var(--muted);font-size:13px;margin-bottom:32px}
|
|
383
|
+
.deck-summary{
|
|
384
|
+
background:var(--surface);border:1px solid var(--border);border-left:4px solid var(--blue);
|
|
385
|
+
padding:16px 20px;border-radius:6px;font-size:15px;font-weight:500;margin-bottom:32px;line-height:1.5;
|
|
386
|
+
}
|
|
387
|
+
.grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;margin-bottom:32px}
|
|
388
|
+
@media(max-width:900px){.grid{grid-template-columns:1fr}}
|
|
389
|
+
.card{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:20px}
|
|
390
|
+
.card-title{font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin-bottom:16px}
|
|
391
|
+
table{width:100%;border-collapse:collapse}
|
|
392
|
+
td{padding:6px 0;border-bottom:1px solid var(--border);font-size:12px;vertical-align:top}
|
|
393
|
+
td:last-child{text-align:right;border-bottom:1px solid var(--border)}
|
|
394
|
+
tr:last-child td{border-bottom:none}
|
|
395
|
+
.val{font-weight:600;font-size:14px}
|
|
396
|
+
.green{color:var(--green)}
|
|
397
|
+
.yellow{color:var(--yellow)}
|
|
398
|
+
.red{color:var(--red)}
|
|
399
|
+
.muted{color:var(--muted)}
|
|
400
|
+
.mono{font-family:monospace;font-size:11px}
|
|
401
|
+
.big-number{font-size:36px;font-weight:700;line-height:1;margin-bottom:4px}
|
|
402
|
+
.big-label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em}
|
|
403
|
+
.hero{display:flex;gap:24px;margin-bottom:32px}
|
|
404
|
+
.hero-item{flex:1;background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:20px}
|
|
405
|
+
.tag-table{width:100%;border-collapse:collapse;margin-top:8px}
|
|
406
|
+
.tag-table th{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);padding:0 0 8px;text-align:left;border-bottom:1px solid var(--border)}
|
|
407
|
+
.tag-table th:not(:first-child){text-align:right}
|
|
408
|
+
.tag-table td{padding:7px 0;border-bottom:1px solid var(--border);font-size:12px}
|
|
409
|
+
.tag-table td:not(:first-child){text-align:right}
|
|
410
|
+
.tag-table tr:last-child td{border-bottom:none}
|
|
411
|
+
.footer{margin-top:40px;font-size:11px;color:var(--muted);display:flex;justify-content:space-between;align-items:center}
|
|
412
|
+
.print-btn{background:var(--blue);color:#fff;border:none;padding:8px 16px;border-radius:6px;cursor:pointer;font-size:12px;font-family:inherit}
|
|
413
|
+
.badge{display:inline-block;font-size:10px;font-weight:600;padding:2px 7px;border-radius:4px;margin-left:6px}
|
|
414
|
+
.badge-green{background:rgba(34,197,94,.15);color:var(--green)}
|
|
415
|
+
.badge-yellow{background:rgba(245,158,11,.15);color:var(--yellow)}
|
|
416
|
+
.badge-red{background:rgba(239,68,68,.12);color:var(--red)}
|
|
417
|
+
/* Comparison table */
|
|
418
|
+
.cmp-wrap{overflow-x:auto;margin-bottom:32px}
|
|
419
|
+
.cmp{width:100%;border-collapse:collapse;font-size:11px}
|
|
420
|
+
.cmp th{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);padding:0 8px 8px 0;text-align:left;border-bottom:2px solid var(--border);white-space:nowrap}
|
|
421
|
+
.cmp td{padding:8px 8px 8px 0;border-bottom:1px solid var(--border);vertical-align:top}
|
|
422
|
+
.cmp tr:last-child td{border-bottom:none}
|
|
423
|
+
.cmp .date{white-space:nowrap;color:var(--muted)}
|
|
424
|
+
.cmp .cmd{font-family:monospace;font-size:10px;background:rgba(255,255,255,.04);padding:2px 5px;border-radius:3px;display:inline-block;max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
425
|
+
.cmp .agree{color:var(--green)}
|
|
426
|
+
.cmp .override{color:var(--yellow)}
|
|
427
|
+
.cmp .unreviewed{color:var(--muted)}
|
|
428
|
+
.cmp .conf{font-size:10px;color:var(--muted)}
|
|
429
|
+
.cmp .reason{font-size:10px;color:var(--yellow);display:block;margin-top:2px}
|
|
430
|
+
</style>
|
|
431
|
+
</head>
|
|
432
|
+
<body>
|
|
433
|
+
<h1>Mergen Impact Report</h1>
|
|
434
|
+
<div class="subtitle">${d.windowStart} \u2192 ${d.windowEnd} \xB7 ${d.windowDays}-day window \xB7 Generated ${new Date(d.generatedAt).toUTCString()}</div>
|
|
435
|
+
|
|
436
|
+
<div class="deck-summary">${d.deckSummary}</div>
|
|
437
|
+
|
|
438
|
+
<div class="hero">
|
|
439
|
+
<div class="hero-item">
|
|
440
|
+
<div class="big-number ${d.wouldResolveRate >= 0.8 ? "green" : d.wouldResolveRate >= 0.6 ? "yellow" : "red"}">${pct(d.wouldResolveRate)}</div>
|
|
441
|
+
<div class="big-label">Autonomous resolution rate</div>
|
|
442
|
+
</div>
|
|
443
|
+
<div class="hero-item">
|
|
444
|
+
<div class="big-number">${num(d.totalIncidents)}</div>
|
|
445
|
+
<div class="big-label">Incidents processed</div>
|
|
446
|
+
</div>
|
|
447
|
+
${d.timeSavedHours !== null ? `
|
|
448
|
+
<div class="hero-item">
|
|
449
|
+
<div class="big-number green">${d.timeSavedHours}h</div>
|
|
450
|
+
<div class="big-label">Engineer time saved${d.hoursPerIncident !== null ? ` \xB7 ${d.hoursPerIncident}h per incident` : ""}</div>
|
|
451
|
+
</div>` : ""}
|
|
452
|
+
${d.estimatedDowntimeSavedUsd !== null ? `
|
|
453
|
+
<div class="hero-item">
|
|
454
|
+
<div class="big-number green">$${d.estimatedDowntimeSavedUsd.toLocaleString()}</div>
|
|
455
|
+
<div class="big-label">Estimated downtime cost prevented</div>
|
|
456
|
+
</div>` : ""}
|
|
457
|
+
<div class="hero-item">
|
|
458
|
+
<div class="big-number green">${fmtMs(d.avgAutonomousMttrMs ?? d.estimatedAutonomousMttrMs)}</div>
|
|
459
|
+
<div class="big-label">${d.avgAutonomousMttrMs !== null ? `Autonomous MTTR (n=${d.autonomousMttrSampleSize})` : "Est. autonomous MTTR"}</div>
|
|
460
|
+
</div>
|
|
461
|
+
${d.avgActualMttrMs !== null ? `
|
|
462
|
+
<div class="hero-item">
|
|
463
|
+
<div class="big-number">${fmtMs(d.avgActualMttrMs)}</div>
|
|
464
|
+
<div class="big-label">Avg. manual MTTR</div>
|
|
465
|
+
</div>` : ""}
|
|
466
|
+
<div class="hero-item">
|
|
467
|
+
<div class="big-number ${d.agentBlunderSummary.totalPrevented > 0 ? "green" : ""}">${num(d.agentBlunderSummary.totalPrevented)}</div>
|
|
468
|
+
<div class="big-label">Destructive actions blocked by gate</div>
|
|
469
|
+
</div>
|
|
470
|
+
</div>
|
|
471
|
+
|
|
472
|
+
${(() => {
|
|
473
|
+
const blunder = d.agentBlunderSummary;
|
|
474
|
+
const chainBadge = blunder.chainVerified ? `<span class="badge badge-green">chain verified</span>` : `<span class="badge badge-yellow">unverified</span>`;
|
|
475
|
+
const typeRows = Object.entries(blunder.byType).sort((a, b) => b[1] - a[1]).map(([t, n]) => `<tr><td class="mono">${t.replace(/_/g, " ")}</td><td class="val">${num(n)}</td></tr>`).join("");
|
|
476
|
+
return `
|
|
477
|
+
<div class="card" style="margin-bottom:32px">
|
|
478
|
+
<div class="card-title">Gate Enforcement (Agent Blunder Log) ${chainBadge}</div>
|
|
479
|
+
<table>
|
|
480
|
+
<tr><td>Total actions blocked by Mergen enforcement gate</td><td class="val ${blunder.totalPrevented > 0 ? "green" : "muted"}">${num(blunder.totalPrevented)}</td></tr>
|
|
481
|
+
${typeRows || '<tr><td class="muted" colspan="2">No blocked actions recorded in this period \u2014 gate active, no agent attempted a destructive action</td></tr>'}
|
|
482
|
+
</table>
|
|
483
|
+
</div>`;
|
|
484
|
+
})()}
|
|
485
|
+
|
|
486
|
+
<div class="grid">
|
|
487
|
+
<div class="card">
|
|
488
|
+
<div class="card-title">Resolution summary</div>
|
|
489
|
+
<table>
|
|
490
|
+
<tr><td>Would resolve autonomously</td><td class="val green">${num(d.wouldResolveCount)}</td></tr>
|
|
491
|
+
<tr><td>Would require manual action</td><td class="val">${num(d.missedCount)}</td></tr>
|
|
492
|
+
${mttrRow}
|
|
493
|
+
${contextAssistedRow}
|
|
494
|
+
${humanRow}
|
|
495
|
+
${overrideRow}
|
|
496
|
+
</table>
|
|
497
|
+
</div>
|
|
498
|
+
<div class="card">
|
|
499
|
+
<div class="card-title">Confidence distribution</div>
|
|
500
|
+
<table>
|
|
501
|
+
<tr>
|
|
502
|
+
<td>HIGH \u2014 would execute (\u226585%)</td>
|
|
503
|
+
<td class="val green">${num(d.highConfidence)}
|
|
504
|
+
<span class="badge badge-green">${d.totalIncidents > 0 ? pct(d.highConfidence / d.totalIncidents) : "0%"}</span>
|
|
505
|
+
</td>
|
|
506
|
+
</tr>
|
|
507
|
+
<tr>
|
|
508
|
+
<td>MEDIUM \u2014 diagnosis only (70\u201384%)</td>
|
|
509
|
+
<td class="val yellow">${num(d.mediumConfidence)}
|
|
510
|
+
<span class="badge badge-yellow">${d.totalIncidents > 0 ? pct(d.mediumConfidence / d.totalIncidents) : "0%"}</span>
|
|
511
|
+
</td>
|
|
512
|
+
</tr>
|
|
513
|
+
<tr>
|
|
514
|
+
<td>LOW / no executable fix (<70%)</td>
|
|
515
|
+
<td class="val muted">${num(d.lowOrNoCommand)}
|
|
516
|
+
<span class="badge badge-red">${d.totalIncidents > 0 ? pct(d.lowOrNoCommand / d.totalIncidents) : "0%"}</span>
|
|
517
|
+
</td>
|
|
518
|
+
</tr>
|
|
519
|
+
</table>
|
|
520
|
+
</div>
|
|
521
|
+
<div class="card">
|
|
522
|
+
<div class="card-title">Calibration & Safety (Platt)</div>
|
|
523
|
+
<table>
|
|
524
|
+
<tr>
|
|
525
|
+
<td>Raw High-Conf. Accuracy</td>
|
|
526
|
+
<td class="val">${d.rawHighConfidenceApprovedRate !== null ? pct(d.rawHighConfidenceApprovedRate) : "\u2014"}</td>
|
|
527
|
+
</tr>
|
|
528
|
+
<tr>
|
|
529
|
+
<td>Platt-Calibrated Accuracy</td>
|
|
530
|
+
<td class="val green">${d.plattHighConfidenceApprovedRate !== null ? pct(d.plattHighConfidenceApprovedRate) : "\u2014"}</td>
|
|
531
|
+
</tr>
|
|
532
|
+
<tr>
|
|
533
|
+
<td>False Positive Rate</td>
|
|
534
|
+
<td class="val red">${d.falsePositiveRate !== null ? pct(d.falsePositiveRate) : "\u2014"}</td>
|
|
535
|
+
</tr>
|
|
536
|
+
<tr>
|
|
537
|
+
<td>False Positives (Overrides)</td>
|
|
538
|
+
<td class="val">${num(d.falsePositiveCount)}</td>
|
|
539
|
+
</tr>
|
|
540
|
+
</table>
|
|
541
|
+
</div>
|
|
542
|
+
</div>
|
|
543
|
+
|
|
544
|
+
${d.executionBlocks.length > 0 ? `
|
|
545
|
+
<div class="card" style="margin-bottom:32px">
|
|
546
|
+
<div class="card-title">Execution Blocks (triggered by ≥85% confidence gate)</div>
|
|
547
|
+
<table class="tag-table">
|
|
548
|
+
<thead>
|
|
549
|
+
<tr>
|
|
550
|
+
<th>Date</th>
|
|
551
|
+
<th>Service</th>
|
|
552
|
+
<th>Command</th>
|
|
553
|
+
<th>Confidence</th>
|
|
554
|
+
<th>Block Source</th>
|
|
555
|
+
<th>Reason</th>
|
|
556
|
+
</tr>
|
|
557
|
+
</thead>
|
|
558
|
+
<tbody>
|
|
559
|
+
${d.executionBlocks.map((b) => `
|
|
560
|
+
<tr>
|
|
561
|
+
<td class="muted">${b.date}</td>
|
|
562
|
+
<td>${b.service}</td>
|
|
563
|
+
<td><code class="mono" style="background:rgba(255,255,255,.04);padding:2px 5px;border-radius:3px">${b.command}</code></td>
|
|
564
|
+
<td>${pct(b.confidence)}</td>
|
|
565
|
+
<td><span class="badge badge-yellow">${b.blockedBy}</span></td>
|
|
566
|
+
<td class="yellow">${b.reason}</td>
|
|
567
|
+
</tr>
|
|
568
|
+
`).join("")}
|
|
569
|
+
</tbody>
|
|
570
|
+
</table>
|
|
571
|
+
</div>` : ""}
|
|
572
|
+
|
|
573
|
+
${d.byTag.length > 0 ? `
|
|
574
|
+
<div class="card" style="margin-bottom:32px">
|
|
575
|
+
<div class="card-title">By failure mode</div>
|
|
576
|
+
<table class="tag-table">
|
|
577
|
+
<thead>
|
|
578
|
+
<tr>
|
|
579
|
+
<th>Failure mode</th>
|
|
580
|
+
<th>Incidents</th>
|
|
581
|
+
<th>Would resolve</th>
|
|
582
|
+
<th>Rate</th>
|
|
583
|
+
<th>Avg. manual MTTR</th>
|
|
584
|
+
</tr>
|
|
585
|
+
</thead>
|
|
586
|
+
<tbody>${tagRows}</tbody>
|
|
587
|
+
</table>
|
|
588
|
+
</div>` : ""}
|
|
589
|
+
|
|
590
|
+
${d.comparisonRows.length > 0 ? buildComparisonTable(d.comparisonRows) : ""}
|
|
591
|
+
|
|
592
|
+
<div class="footer">
|
|
593
|
+
<span>Mergen shadow mode \xB7 <a href="/shadow-report">shadow-report</a> \xB7 <a href="/override-corpus">override-corpus</a></span>
|
|
594
|
+
<button class="print-btn no-print" onclick="window.print()">Save as PDF</button>
|
|
595
|
+
</div>
|
|
596
|
+
</body>
|
|
597
|
+
</html>`;
|
|
598
|
+
}
|
|
599
|
+
function buildComparisonTable(rows) {
|
|
600
|
+
const agreeIcon = "\u2713";
|
|
601
|
+
const overrideIcon = "\u2195";
|
|
602
|
+
const pendingIcon = "\xB7";
|
|
603
|
+
const trs = rows.map((r) => {
|
|
604
|
+
const cls = r.agreementType;
|
|
605
|
+
const icon = cls === "agree" ? agreeIcon : cls === "override" ? overrideIcon : pendingIcon;
|
|
606
|
+
const cmdCell = r.mergenCommand ? `<span class="cmd" title="${r.mergenCommand}">${r.mergenCommand}</span>` : '<span class="muted">No executable fix</span>';
|
|
607
|
+
const trustBadge = r.mergenCommand ? `<span class="badge badge-green" style="margin-left:0; margin-top:4px; display:inline-block;">\u{1F6E1} Empirically Verified (${Math.round(r.mergenConfidence * 100)}% accuracy)</span>` : `<span class="conf">${Math.round(r.mergenConfidence * 100)}% remediation confidence</span>`;
|
|
608
|
+
const engineerCell = cls === "unreviewed" ? `<span class="unreviewed">Not yet reviewed \u2014 POST /shadow-report/${r.service}/verdict</span>` : r.engineerAction;
|
|
609
|
+
const reasonSpan = r.overrideReason ? `<span class="reason">${r.overrideReason}</span>` : "";
|
|
610
|
+
const mttr = r.actualMttrMs !== null ? fmtMs(r.actualMttrMs) : "\u2014";
|
|
611
|
+
const ghostTimeline = r.actualMttrMs !== null ? `<div style="margin-top:6px;font-size:10px;padding-left:6px;border-left:2px solid var(--border);color:var(--muted)">
|
|
612
|
+
<i>Manual: ${fmtMs(r.actualMttrMs)} (Page \u2192 Wake up \u2192 Diagnose)</i><br>
|
|
613
|
+
<i>Mergen: ~2m (Trigger \u2192 Fix)</i>
|
|
614
|
+
</div>` : "";
|
|
615
|
+
const outcome = r.outcome ?? "\u2014";
|
|
616
|
+
return `<tr>
|
|
617
|
+
<td class="date">${r.date}</td>
|
|
618
|
+
<td>${r.service}<br><span class="conf">${r.failureMode}</span></td>
|
|
619
|
+
<td>${cmdCell}<br>${trustBadge}</td>
|
|
620
|
+
<td class="${cls}">${icon} ${engineerCell}${reasonSpan}</td>
|
|
621
|
+
<td class="muted">${mttr}${ghostTimeline}</td>
|
|
622
|
+
<td class="muted">${outcome}</td>
|
|
623
|
+
</tr>`;
|
|
624
|
+
}).join("");
|
|
625
|
+
const unreviewedCount = rows.filter((r) => r.agreementType === "unreviewed").length;
|
|
626
|
+
const unreviewedNote = unreviewedCount > 0 ? `<p style="font-size:11px;color:var(--muted);margin-top:10px">${unreviewedCount} incident${unreviewedCount !== 1 ? "s" : ""} not yet reviewed. Use <code>POST /shadow-report/:id/verdict</code> to annotate \u2014 "would-approve" or "would-override".</p>` : "";
|
|
627
|
+
return `<div class="card cmp-wrap" style="margin-bottom:32px">
|
|
628
|
+
<div class="card-title">Incident comparison \u2014 what Mergen would have done vs. what happened</div>
|
|
629
|
+
<table class="cmp">
|
|
630
|
+
<thead><tr>
|
|
631
|
+
<th>Date</th>
|
|
632
|
+
<th>Service / Failure mode</th>
|
|
633
|
+
<th>Mergen's proposed action</th>
|
|
634
|
+
<th>Engineer's action</th>
|
|
635
|
+
<th>Actual MTTR</th>
|
|
636
|
+
<th>Outcome</th>
|
|
637
|
+
</tr></thead>
|
|
638
|
+
<tbody>${trs}</tbody>
|
|
639
|
+
</table>
|
|
640
|
+
${unreviewedNote}
|
|
641
|
+
</div>`;
|
|
642
|
+
}
|
|
643
|
+
export {
|
|
644
|
+
createImpactReportRouter
|
|
645
|
+
};
|