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.
Files changed (308) hide show
  1. package/LICENSE +57 -0
  2. package/README.md +96 -0
  3. package/dist/__stubs__/calibration.js +471 -0
  4. package/dist/__stubs__/causal.js +601 -0
  5. package/dist/__stubs__/closed-source.js +150 -0
  6. package/dist/__stubs__/license.js +35 -0
  7. package/dist/__stubs__/plans.js +197 -0
  8. package/dist/app.js +467 -0
  9. package/dist/cli.js +250 -0
  10. package/dist/commands/agent-identity.js +79 -0
  11. package/dist/commands/gate.js +107 -0
  12. package/dist/commands/github.js +632 -0
  13. package/dist/commands/incident.js +644 -0
  14. package/dist/commands/policy.js +710 -0
  15. package/dist/commands/scan.js +243 -0
  16. package/dist/commands/setup.js +1218 -0
  17. package/dist/commands/shared.js +109 -0
  18. package/dist/commands/team.js +809 -0
  19. package/dist/datadog/blame-attribution.js +228 -0
  20. package/dist/datadog/client.js +159 -0
  21. package/dist/datadog/compactor.js +169 -0
  22. package/dist/datadog/fingerprinter.js +34 -0
  23. package/dist/datadog/incident-state.js +21 -0
  24. package/dist/datadog/line-matcher.js +56 -0
  25. package/dist/datadog/memory-store.js +399 -0
  26. package/dist/datadog/otel-trace.js +67 -0
  27. package/dist/index.js +505 -0
  28. package/dist/intelligence/action-risk.js +132 -0
  29. package/dist/intelligence/activity-feed.js +40 -0
  30. package/dist/intelligence/agent-identity.js +115 -0
  31. package/dist/intelligence/agent-pipeline.js +329 -0
  32. package/dist/intelligence/agent-profiles.js +81 -0
  33. package/dist/intelligence/ai-commit.js +16 -0
  34. package/dist/intelligence/approval-analytics.js +42 -0
  35. package/dist/intelligence/approval-events.js +5 -0
  36. package/dist/intelligence/arch-boundaries.js +94 -0
  37. package/dist/intelligence/arch-graph.js +117 -0
  38. package/dist/intelligence/autonomy.js +397 -0
  39. package/dist/intelligence/azure-broker.js +57 -0
  40. package/dist/intelligence/azure-proxy-sessions.js +49 -0
  41. package/dist/intelligence/baseline.js +1 -0
  42. package/dist/intelligence/behavior-baseline.js +134 -0
  43. package/dist/intelligence/billing.js +1 -0
  44. package/dist/intelligence/blast-radius.js +224 -0
  45. package/dist/intelligence/bypass.js +157 -0
  46. package/dist/intelligence/calibration-classifier.js +77 -0
  47. package/dist/intelligence/calibration-git-sync.js +74 -0
  48. package/dist/intelligence/calibration.js +1 -0
  49. package/dist/intelligence/cascade-detector.js +58 -0
  50. package/dist/intelligence/case-study-generator.js +126 -0
  51. package/dist/intelligence/causal-graph.js +126 -0
  52. package/dist/intelligence/causal.js +1 -0
  53. package/dist/intelligence/change-risk.js +102 -0
  54. package/dist/intelligence/compliance-report.js +134 -0
  55. package/dist/intelligence/confidence-report.js +85 -0
  56. package/dist/intelligence/corpus-to-policy.js +140 -0
  57. package/dist/intelligence/credential-broker.js +112 -0
  58. package/dist/intelligence/credential-misuse.js +39 -0
  59. package/dist/intelligence/debug-sessions.js +108 -0
  60. package/dist/intelligence/decision-escalation.js +36 -0
  61. package/dist/intelligence/decision-memory.js +47 -0
  62. package/dist/intelligence/decision-similarity.js +85 -0
  63. package/dist/intelligence/default-safety-tests.js +53 -0
  64. package/dist/intelligence/degradation-watcher.js +108 -0
  65. package/dist/intelligence/detector-plugins.js +70 -0
  66. package/dist/intelligence/detectors.js +1 -0
  67. package/dist/intelligence/device-auth.js +138 -0
  68. package/dist/intelligence/diff-size.js +42 -0
  69. package/dist/intelligence/ebpf-verify.js +45 -0
  70. package/dist/intelligence/enterprise-policy-engine.js +1122 -0
  71. package/dist/intelligence/error-fingerprint.js +1 -0
  72. package/dist/intelligence/execution-gate.js +100 -0
  73. package/dist/intelligence/execution-mode.js +22 -0
  74. package/dist/intelligence/gate-analytics.js +256 -0
  75. package/dist/intelligence/gate-decision.js +64 -0
  76. package/dist/intelligence/gcp-broker.js +50 -0
  77. package/dist/intelligence/git-adr-sync.js +207 -0
  78. package/dist/intelligence/hitl-hold.js +460 -0
  79. package/dist/intelligence/hypothesis-history.js +93 -0
  80. package/dist/intelligence/idp-identity.js +83 -0
  81. package/dist/intelligence/impl-critic.js +216 -0
  82. package/dist/intelligence/incident-autopilot.js +622 -0
  83. package/dist/intelligence/incident-replay.js +162 -0
  84. package/dist/intelligence/incident-result-cache.js +33 -0
  85. package/dist/intelligence/incident-similarity.js +58 -0
  86. package/dist/intelligence/infra-detectors.js +312 -0
  87. package/dist/intelligence/license.js +185 -0
  88. package/dist/intelligence/llm-spokesperson.js +62 -0
  89. package/dist/intelligence/mcp-prompts.js +320 -0
  90. package/dist/intelligence/mcp-resources.js +171 -0
  91. package/dist/intelligence/normalize.js +108 -0
  92. package/dist/intelligence/notifications.js +83 -0
  93. package/dist/intelligence/oidc-issuer.js +87 -0
  94. package/dist/intelligence/override-corpus.js +597 -0
  95. package/dist/intelligence/planning-gate.js +84 -0
  96. package/dist/intelligence/plans.js +223 -0
  97. package/dist/intelligence/platt-scaling.js +164 -0
  98. package/dist/intelligence/policy-proposals.js +84 -0
  99. package/dist/intelligence/policy-suggester.js +161 -0
  100. package/dist/intelligence/policy-sync.js +103 -0
  101. package/dist/intelligence/policy-test-runner.js +35 -0
  102. package/dist/intelligence/postmortem-parser.js +181 -0
  103. package/dist/intelligence/postmortem-retrieval.js +224 -0
  104. package/dist/intelligence/postmortem-store.js +596 -0
  105. package/dist/intelligence/pr-commenter.js +81 -0
  106. package/dist/intelligence/pr-policy-gate.js +38 -0
  107. package/dist/intelligence/pr-shadow-analyzer.js +229 -0
  108. package/dist/intelligence/process-tree.js +108 -0
  109. package/dist/intelligence/prompts.js +1 -0
  110. package/dist/intelligence/replay.js +38 -0
  111. package/dist/intelligence/repro-steps.js +1 -0
  112. package/dist/intelligence/resource-extractors.js +0 -0
  113. package/dist/intelligence/resource-file-context.js +138 -0
  114. package/dist/intelligence/risk-score.js +22 -0
  115. package/dist/intelligence/rollback.js +137 -0
  116. package/dist/intelligence/runbook-updater.js +143 -0
  117. package/dist/intelligence/sandbox.js +194 -0
  118. package/dist/intelligence/script-trust.js +104 -0
  119. package/dist/intelligence/session-metrics.js +67 -0
  120. package/dist/intelligence/session-threat-tracker.js +231 -0
  121. package/dist/intelligence/shadow-digest-cron.js +81 -0
  122. package/dist/intelligence/shadow-log.js +284 -0
  123. package/dist/intelligence/shell-ast.js +145 -0
  124. package/dist/intelligence/siem-forward.js +104 -0
  125. package/dist/intelligence/slack-digest.js +151 -0
  126. package/dist/intelligence/slack-override-loop.js +217 -0
  127. package/dist/intelligence/slack-routing.js +67 -0
  128. package/dist/intelligence/slack.js +900 -0
  129. package/dist/intelligence/sql-ast.js +31 -0
  130. package/dist/intelligence/team.js +1 -0
  131. package/dist/intelligence/telemetry.js +76 -0
  132. package/dist/intelligence/threshold-optimizer.js +75 -0
  133. package/dist/intelligence/token-budget.js +66 -0
  134. package/dist/intelligence/tool-guard.js +708 -0
  135. package/dist/intelligence/tool-manifest 2.js +74 -0
  136. package/dist/intelligence/tool-manifest.js +125 -0
  137. package/dist/intelligence/tools-analysis.js +857 -0
  138. package/dist/intelligence/tools-arch.js +152 -0
  139. package/dist/intelligence/tools-autonomy.js +495 -0
  140. package/dist/intelligence/tools-blast-radius.js +140 -0
  141. package/dist/intelligence/tools-browser.js +431 -0
  142. package/dist/intelligence/tools-change-timeline.js +154 -0
  143. package/dist/intelligence/tools-credentials.js +117 -0
  144. package/dist/intelligence/tools-datadog.js +233 -0
  145. package/dist/intelligence/tools-debug-sessions.js +209 -0
  146. package/dist/intelligence/tools-discovery.js +284 -0
  147. package/dist/intelligence/tools-infra.js +612 -0
  148. package/dist/intelligence/tools-intent.js +128 -0
  149. package/dist/intelligence/tools-memory.js +514 -0
  150. package/dist/intelligence/tools-runbook.js +951 -0
  151. package/dist/intelligence/tools-sessions.js +107 -0
  152. package/dist/intelligence/tools-state.js +232 -0
  153. package/dist/intelligence/tools-utility.js +430 -0
  154. package/dist/intelligence/tools-validate.js +215 -0
  155. package/dist/intelligence/tools.js +1 -0
  156. package/dist/intelligence/trace-context.js +11 -0
  157. package/dist/intelligence/unclassified-clusters.js +87 -0
  158. package/dist/intelligence/usage.js +195 -0
  159. package/dist/routes/active-authors.js +63 -0
  160. package/dist/routes/activity-feed.js +21 -0
  161. package/dist/routes/adr.js +117 -0
  162. package/dist/routes/agent-activity.js +184 -0
  163. package/dist/routes/agent-blunders.js +163 -0
  164. package/dist/routes/agents.js +175 -0
  165. package/dist/routes/api-keys.js +65 -0
  166. package/dist/routes/arch.js +75 -0
  167. package/dist/routes/audit-export.js +93 -0
  168. package/dist/routes/billing-dashboard.js +158 -0
  169. package/dist/routes/billing-outcome.js +77 -0
  170. package/dist/routes/calibration.js +305 -0
  171. package/dist/routes/ci-gate-history.js +20 -0
  172. package/dist/routes/ci-gate.js +232 -0
  173. package/dist/routes/ci.js +293 -0
  174. package/dist/routes/compliance-report.js +24 -0
  175. package/dist/routes/confidence.js +30 -0
  176. package/dist/routes/credentials.js +119 -0
  177. package/dist/routes/dashboard.js +1407 -0
  178. package/dist/routes/decision-memory.js +42 -0
  179. package/dist/routes/demo.js +844 -0
  180. package/dist/routes/device-auth-stub.js +173 -0
  181. package/dist/routes/explain-why.js +39 -0
  182. package/dist/routes/gate-analytics.js +213 -0
  183. package/dist/routes/gate.js +28 -0
  184. package/dist/routes/github-webhook.js +364 -0
  185. package/dist/routes/habituation.js +30 -0
  186. package/dist/routes/health-integrations.js +243 -0
  187. package/dist/routes/heartbeats.js +45 -0
  188. package/dist/routes/hitl.js +364 -0
  189. package/dist/routes/impact-report.js +645 -0
  190. package/dist/routes/incident-webhook.js +63 -0
  191. package/dist/routes/incidents.js +366 -0
  192. package/dist/routes/layers.js +62 -0
  193. package/dist/routes/license.js +187 -0
  194. package/dist/routes/oidc-issuer.js +30 -0
  195. package/dist/routes/onboarding.js +170 -0
  196. package/dist/routes/otel.js +67 -0
  197. package/dist/routes/otlp-receiver.js +179 -0
  198. package/dist/routes/overrides.js +241 -0
  199. package/dist/routes/pagerduty.js +258 -0
  200. package/dist/routes/policies.js +809 -0
  201. package/dist/routes/policy-nl.js +169 -0
  202. package/dist/routes/postmortem.js +102 -0
  203. package/dist/routes/pr-shadow.js +47 -0
  204. package/dist/routes/rbac.js +61 -0
  205. package/dist/routes/replay.js +17 -0
  206. package/dist/routes/risk-report.js +164 -0
  207. package/dist/routes/runbooks.js +125 -0
  208. package/dist/routes/safety-test.js +174 -0
  209. package/dist/routes/sdk.js +222 -0
  210. package/dist/routes/sensor.js +819 -0
  211. package/dist/routes/sentry.js +140 -0
  212. package/dist/routes/sessions.js +43 -0
  213. package/dist/routes/setup-ui.js +594 -0
  214. package/dist/routes/shadow-report.js +107 -0
  215. package/dist/routes/slack-routing.js +171 -0
  216. package/dist/routes/team-usage.js +71 -0
  217. package/dist/routes/telemetry.js +28 -0
  218. package/dist/routes/tenants.js +199 -0
  219. package/dist/routes/tickets.js +167 -0
  220. package/dist/routes/validate.js +13 -0
  221. package/dist/routes/war-room.js +113 -0
  222. package/dist/scripts/check-arch.js +22 -0
  223. package/dist/seeds/community-corpus.js +176 -0
  224. package/dist/seeds/corpus.js +1224 -0
  225. package/dist/sensor/action-ledger.js +327 -0
  226. package/dist/sensor/adr-store.js +140 -0
  227. package/dist/sensor/agent-blunder-store.js +370 -0
  228. package/dist/sensor/agent-context-store.js +225 -0
  229. package/dist/sensor/agent-memory-store.js +231 -0
  230. package/dist/sensor/audit-fetch.js +23 -0
  231. package/dist/sensor/audit-log.js +273 -0
  232. package/dist/sensor/buffer-schemas.js +225 -0
  233. package/dist/sensor/buffer.js +599 -0
  234. package/dist/sensor/bypass-tracker.js +93 -0
  235. package/dist/sensor/ci-gate-history.js +70 -0
  236. package/dist/sensor/cloud-auth.js +183 -0
  237. package/dist/sensor/commit-context-store.js +235 -0
  238. package/dist/sensor/docker-log-stream.js +164 -0
  239. package/dist/sensor/docker-monitor.js +122 -0
  240. package/dist/sensor/extended-buffer.js +46 -0
  241. package/dist/sensor/feedback-token.js +41 -0
  242. package/dist/sensor/file-lock.js +6 -0
  243. package/dist/sensor/fs-watcher.js +106 -0
  244. package/dist/sensor/gate-heartbeat.js +122 -0
  245. package/dist/sensor/git-suspect.js +136 -0
  246. package/dist/sensor/habituation-store.js +77 -0
  247. package/dist/sensor/heartbeat-monitor.js +133 -0
  248. package/dist/sensor/incident-store.js +340 -0
  249. package/dist/sensor/infra-normalizer.js +513 -0
  250. package/dist/sensor/ingest.js +339 -0
  251. package/dist/sensor/jest-reporter.js +52 -0
  252. package/dist/sensor/k8s-events.js +132 -0
  253. package/dist/sensor/layer2-store.js +111 -0
  254. package/dist/sensor/layer3-store.js +230 -0
  255. package/dist/sensor/layer4-store.js +147 -0
  256. package/dist/sensor/logger.js +13 -0
  257. package/dist/sensor/otel-exporter.js +161 -0
  258. package/dist/sensor/paths.js +71 -0
  259. package/dist/sensor/policy-history.js +147 -0
  260. package/dist/sensor/pr-shadow-store.js +83 -0
  261. package/dist/sensor/process-watcher.js +162 -0
  262. package/dist/sensor/rbac.js +92 -0
  263. package/dist/sensor/redact.js +114 -0
  264. package/dist/sensor/redis-store.js +191 -0
  265. package/dist/sensor/route-reachability.js +64 -0
  266. package/dist/sensor/security-utils.js +18 -0
  267. package/dist/sensor/service-graph.js +170 -0
  268. package/dist/sensor/service-topology.js +227 -0
  269. package/dist/sensor/session-history.js +74 -0
  270. package/dist/sensor/session-persist.js +38 -0
  271. package/dist/sensor/shadow-promote.js +122 -0
  272. package/dist/sensor/sourcemap.js +281 -0
  273. package/dist/sensor/sqlite-store.js +205 -0
  274. package/dist/sensor/sso.js +164 -0
  275. package/dist/sensor/vitest-reporter.js +65 -0
  276. package/dist/sensor/watcher.js +48 -0
  277. package/dist/storage/interfaces.js +0 -0
  278. package/dist/storage/pg/pg-action-ledger.js +138 -0
  279. package/dist/storage/pg/pg-approval-store.js +107 -0
  280. package/dist/storage/pg/pg-blunder-store.js +193 -0
  281. package/dist/storage/pg/pg-ci-gate-history.js +83 -0
  282. package/dist/storage/pg/pg-client.js +24 -0
  283. package/dist/storage/pg/pg-event-store.js +63 -0
  284. package/dist/storage/pg/pg-incident-store.js +190 -0
  285. package/dist/storage/pg/pg-migrations.js +26 -0
  286. package/dist/storage/pg/pg-override-corpus.js +447 -0
  287. package/dist/storage/pg/pg-shadow-log.js +129 -0
  288. package/dist/storage/sqlite/sqlite-action-ledger.js +18 -0
  289. package/dist/storage/sqlite/sqlite-approval-store.js +37 -0
  290. package/dist/storage/sqlite/sqlite-blunder-store.js +27 -0
  291. package/dist/storage/sqlite/sqlite-ci-gate-history.js +19 -0
  292. package/dist/storage/sqlite/sqlite-event-store.js +29 -0
  293. package/dist/storage/sqlite/sqlite-incident-store.js +34 -0
  294. package/dist/storage/sqlite/sqlite-override-corpus.js +75 -0
  295. package/dist/storage/sqlite/sqlite-shadow-log.js +36 -0
  296. package/dist/storage/store-factory.js +55 -0
  297. package/dist/storage/store-registry.js +31 -0
  298. package/dist/update-checker.js +107 -0
  299. package/dist/workers/autopilot-worker.js +28 -0
  300. package/dist/workers/notification-worker.js +28 -0
  301. package/dist/workers/queues.js +58 -0
  302. package/dist/workers/validation-worker.js +24 -0
  303. package/dist/workers/worker-registry.js +18 -0
  304. package/package.json +123 -0
  305. package/sdk/mergen-inject.js +260 -0
  306. package/sdk/node.js +313 -0
  307. package/sdk/vite-plugin.ts +57 -0
  308. package/sdk/webpack-plugin.js +106 -0
@@ -0,0 +1,158 @@
1
+ import { Router } from "express";
2
+ import { getLicenseState, getActivePlanId } from "../intelligence/license.js";
3
+ import { getPlan, PLANS, estimateToolCallOverageCents, estimateSeatOverageCents } from "../intelligence/plans.js";
4
+ import { getUsageSnapshot } from "../intelligence/usage.js";
5
+ import { getToolCallCounts } from "../intelligence/gate-analytics.js";
6
+ import { listMembers } from "../sensor/rbac.js";
7
+ function totalMediatedToolCalls() {
8
+ let total = 0;
9
+ for (const count of getToolCallCounts().values()) total += count;
10
+ return total;
11
+ }
12
+ const DOCS_URL = "https://mergen.app/pricing";
13
+ function nextResetLabel() {
14
+ const now = /* @__PURE__ */ new Date();
15
+ const next = new Date(now.getFullYear(), now.getMonth() + 1, 1);
16
+ const diffDays = Math.ceil((next.getTime() - now.getTime()) / 864e5);
17
+ return `${next.toISOString().slice(0, 10)} (${diffDays} day${diffDays !== 1 ? "s" : ""})`;
18
+ }
19
+ function usagePercent(used, included) {
20
+ if (included === null || included === 0) return null;
21
+ return Math.min(100, Math.round(used / included * 100));
22
+ }
23
+ function createBillingDashboardRouter() {
24
+ const router = Router();
25
+ router.get("/billing/dashboard", (_req, res) => {
26
+ const planId = getActivePlanId();
27
+ const plan = getPlan(planId);
28
+ const state = getLicenseState();
29
+ const usage = getUsageSnapshot();
30
+ const isPaid = planId !== "free";
31
+ const isPayg = planId === "pay_as_you_go";
32
+ const pctUsed = usagePercent(usage.used, usage.included);
33
+ const nearLimit = pctUsed !== null && pctUsed >= 80 && !isPayg;
34
+ const atLimit = pctUsed !== null && pctUsed >= 100 && !isPayg;
35
+ const overageCostCents = usage.estimatedOverageCents;
36
+ const overageCostDollars = overageCostCents > 0 ? (overageCostCents / 100).toFixed(2) : null;
37
+ const toolCallsThisMonth = totalMediatedToolCalls();
38
+ const toolCallOverageCents = estimateToolCallOverageCents(planId, toolCallsThisMonth);
39
+ const activeSeats = listMembers().length;
40
+ const seatOverageCents = estimateSeatOverageCents(planId, activeSeats);
41
+ let upgradeMessage = null;
42
+ let upgradePlan = null;
43
+ if (atLimit) {
44
+ upgradeMessage = `You've reached your ${usage.included} credit limit for ${usage.month}. Upgrade to keep running analyses.`;
45
+ upgradePlan = plan.ctaUrl != null ? String(plan.ctaUrl) : null;
46
+ } else if (nearLimit && usage.remaining !== null) {
47
+ upgradeMessage = `${usage.remaining} credits remaining this month. Consider upgrading to avoid interruption.`;
48
+ upgradePlan = plan.ctaUrl != null ? String(plan.ctaUrl) : null;
49
+ } else if (!isPaid) {
50
+ upgradeMessage = "Upgrade to unlock backend observability, more credits, and autonomous execution.";
51
+ upgradePlan = DOCS_URL;
52
+ }
53
+ res.json({
54
+ ok: true,
55
+ plan: {
56
+ id: plan.id,
57
+ name: plan.name,
58
+ tagline: plan.tagline,
59
+ seats: plan.seats,
60
+ isPaid,
61
+ ctaUrl: plan.ctaUrl
62
+ },
63
+ license: state ? {
64
+ email: state.customerEmail ?? null,
65
+ status: state.status ?? "active",
66
+ activatedAt: state.validatedAt ?? null
67
+ } : null,
68
+ usage: {
69
+ month: usage.month,
70
+ used: usage.used,
71
+ included: usage.included,
72
+ remaining: usage.remaining,
73
+ percentUsed: pctUsed,
74
+ overageCredits: usage.overage,
75
+ overagePending: usage.overagePendingCredits,
76
+ estimatedOverageDollars: overageCostDollars,
77
+ nearLimit,
78
+ atLimit
79
+ },
80
+ // Execution-volume usage — mediated tool-calls (Gate A evaluations), the
81
+ // primary metering dimension alongside seats (analyze-credits above cover
82
+ // diagnostic AI checks specifically, a separate, narrower usage type).
83
+ execution: {
84
+ toolCallsThisMonth,
85
+ includedToolCallsPerMonth: plan.includedToolCallsPerMonth === 0 ? null : plan.includedToolCallsPerMonth,
86
+ estimatedOverageDollars: toolCallOverageCents > 0 ? (toolCallOverageCents / 100).toFixed(2) : null
87
+ },
88
+ seats: {
89
+ active: activeSeats,
90
+ included: isFinite(plan.seatCap) ? plan.seatCap : null,
91
+ estimatedOverageDollars: seatOverageCents > 0 ? (seatOverageCents / 100).toFixed(2) : null
92
+ },
93
+ billing: {
94
+ nextResetOn: nextResetLabel(),
95
+ overageCentsPerCredit: plan.overageCentsPerCredit,
96
+ docsUrl: DOCS_URL
97
+ },
98
+ upgrade: upgradeMessage ? { message: upgradeMessage, url: upgradePlan } : null
99
+ });
100
+ });
101
+ router.get("/billing/plans", (_req, res) => {
102
+ const activePlanId = getActivePlanId();
103
+ const plans = Object.values(PLANS).map((p) => ({
104
+ id: p.id,
105
+ name: p.name,
106
+ tagline: p.tagline,
107
+ seats: p.seats,
108
+ bufferSize: p.bufferSize,
109
+ backendObservability: p.backendObservability,
110
+ analyzeCreditsPerMonth: p.analyzeCreditsPerMonth === Infinity ? null : p.analyzeCreditsPerMonth,
111
+ overageCentsPerCredit: p.overageCentsPerCredit,
112
+ overagePriceLabel: p.overageCentsPerCredit > 0 ? `$${(p.overageCentsPerCredit / 100).toFixed(2)} / credit over limit` : "No overage \u2014 hard stop at limit",
113
+ seatCap: isFinite(p.seatCap) ? p.seatCap : null,
114
+ overageCentsPerSeat: p.overageCentsPerSeat,
115
+ seatOveragePriceLabel: p.overageCentsPerSeat > 0 ? `$${(p.overageCentsPerSeat / 100).toFixed(2)} / mo per additional seat` : null,
116
+ includedToolCallsPerMonth: p.includedToolCallsPerMonth === 0 ? null : p.includedToolCallsPerMonth,
117
+ overageCentsPerThousandToolCalls: p.overageCentsPerThousandToolCalls,
118
+ toolCallOveragePriceLabel: p.overageCentsPerThousandToolCalls > 0 ? `$${(p.overageCentsPerThousandToolCalls / 100).toFixed(2)} / 1,000 tool-calls over limit` : null,
119
+ ctaUrl: p.ctaUrl,
120
+ active: p.id === activePlanId
121
+ }));
122
+ res.json({ ok: true, plans, activePlanId });
123
+ });
124
+ router.get("/billing/usage", (_req, res) => {
125
+ const planId = getActivePlanId();
126
+ const plan = getPlan(planId);
127
+ const usage = getUsageSnapshot();
128
+ const pctUsed = usagePercent(usage.used, usage.included);
129
+ const status = !pctUsed ? "ok" : pctUsed >= 100 ? "at_limit" : pctUsed >= 80 ? "near_limit" : "ok";
130
+ res.json({
131
+ ok: true,
132
+ current: {
133
+ month: usage.month,
134
+ used: usage.used,
135
+ included: usage.included,
136
+ remaining: usage.remaining,
137
+ percentUsed: pctUsed,
138
+ overage: usage.overage,
139
+ overagePendingCredits: usage.overagePendingCredits,
140
+ overageConfirmedCredits: usage.overageConfirmedCredits,
141
+ estimatedOverageCents: usage.estimatedOverageCents,
142
+ estimatedOverageDollars: usage.estimatedOverageCents > 0 ? (usage.estimatedOverageCents / 100).toFixed(2) : null,
143
+ status
144
+ },
145
+ plan: {
146
+ id: plan.id,
147
+ name: plan.name,
148
+ overageCentsPerCredit: plan.overageCentsPerCredit,
149
+ ctaUrl: plan.ctaUrl
150
+ },
151
+ guidance: status === "at_limit" ? { message: `Monthly limit reached. Upgrade or wait for reset on ${nextResetLabel()}.`, url: plan.ctaUrl } : status === "near_limit" ? { message: `Approaching limit (${pctUsed}% used). Consider upgrading.`, url: plan.ctaUrl } : null
152
+ });
153
+ });
154
+ return router;
155
+ }
156
+ export {
157
+ createBillingDashboardRouter
158
+ };
@@ -0,0 +1,77 @@
1
+ import { Router } from "express";
2
+ import { getStores } from "../storage/store-registry.js";
3
+ import { postmortemStore } from "../intelligence/postmortem-store.js";
4
+ import { getIncidentCount } from "../intelligence/usage.js";
5
+ const DEFAULT_REVENUE_PER_MINUTE_USD = 100;
6
+ const AUTONOMOUS_MTTR_FALLBACK_MS = 2 * 60 * 1e3;
7
+ function getMedianAutonomousMttrMs(resolved) {
8
+ const samples = resolved.filter((i) => i.resolvedAutonomously && i.resolvedAt != null).map((i) => i.resolvedAt - i.createdAt).filter((ms) => ms > 0).sort((a, b) => a - b);
9
+ if (samples.length < 3) return null;
10
+ const mid = Math.floor(samples.length / 2);
11
+ return samples.length % 2 === 0 ? (samples[mid - 1] + samples[mid]) / 2 : samples[mid];
12
+ }
13
+ function fmtMs(ms) {
14
+ if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
15
+ const min = Math.floor(ms / 6e4);
16
+ const sec = Math.round(ms % 6e4 / 1e3);
17
+ return sec > 0 ? `${min}m ${sec}s` : `${min}m`;
18
+ }
19
+ function createBillingOutcomeRouter() {
20
+ const router = Router();
21
+ router.get("/billing/outcome-report", async (req, res) => {
22
+ const revenuePerMinute = parseFloat(process.env.MERGEN_REVENUE_PER_MINUTE_USD ?? "") || DEFAULT_REVENUE_PER_MINUTE_USD;
23
+ const all = await getStores().incidents.list(void 0, 1e3, req.tenantId);
24
+ const resolved = all.filter((i) => i.status === "resolved" && i.resolvedAt && i.createdAt);
25
+ const manualMttrSamples = resolved.filter((i) => !i.resolvedAutonomously).map((i) => i.resolvedAt - i.createdAt);
26
+ const avgManualMttrMs = manualMttrSamples.length > 0 ? manualMttrSamples.reduce((a, b) => a + b, 0) / manualMttrSamples.length : null;
27
+ const autonomousCount = resolved.filter((i) => i.resolvedAutonomously).length;
28
+ const totalResolved = resolved.length;
29
+ const empiricalAutonomousMttrMs = getMedianAutonomousMttrMs(resolved);
30
+ const autonomousMttrMs = empiricalAutonomousMttrMs ?? AUTONOMOUS_MTTR_FALLBACK_MS;
31
+ const autonomousMttrSource = empiricalAutonomousMttrMs != null ? `empirical median (n=${resolved.filter((i) => i.resolvedAutonomously && i.resolvedAt).length})` : "conservative prior \u2014 updates after 3+ autonomous resolutions";
32
+ const baselineMttrMs = avgManualMttrMs ?? 30 * 60 * 1e3;
33
+ const mttrSavedPerIncidentMs = Math.max(0, baselineMttrMs - autonomousMttrMs);
34
+ const totalTimeSavedMs = mttrSavedPerIncidentMs * autonomousCount;
35
+ const totalTimeSavedMin = totalTimeSavedMs / 6e4;
36
+ const estimatedRevenuePreservedUsd = totalTimeSavedMin * revenuePerMinute;
37
+ const corpusTotal = postmortemStore.count();
38
+ const tagStats = postmortemStore.tagStats();
39
+ const serviceSet = new Set(all.map((i) => i.service ?? "unknown").filter((s) => s !== "unknown"));
40
+ const incidentsThisMonth = getIncidentCount();
41
+ const report = {
42
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
43
+ // Core SRE metrics
44
+ totalResolved,
45
+ autonomousResolutions: autonomousCount,
46
+ autonomousRate: totalResolved > 0 ? Math.round(autonomousCount / totalResolved * 100) : 0,
47
+ avgManualMttrMs,
48
+ avgManualMttrLabel: avgManualMttrMs != null ? fmtMs(avgManualMttrMs) : null,
49
+ estimatedAutonomousMttrMs: autonomousMttrMs,
50
+ autonomousMttrSource,
51
+ // Time savings
52
+ mttrSavedPerIncidentMs: avgManualMttrMs != null ? mttrSavedPerIncidentMs : null,
53
+ totalTimeSavedMs: avgManualMttrMs != null ? totalTimeSavedMs : null,
54
+ totalTimeSavedLabel: avgManualMttrMs != null ? fmtMs(totalTimeSavedMs) : null,
55
+ // Revenue preservation (Y5 outcome billing)
56
+ revenuePerMinuteUsd: revenuePerMinute,
57
+ estimatedRevenuePreservedUsd: avgManualMttrMs != null ? Math.round(estimatedRevenuePreservedUsd) : null,
58
+ revenuePreservedNote: avgManualMttrMs == null ? "Insufficient data: need at least one manually-resolved incident for MTTR baseline." : `Based on ${fmtMs(baselineMttrMs)} avg manual MTTR vs ${fmtMs(autonomousMttrMs)} autonomous (${autonomousMttrSource}). Configure MERGEN_REVENUE_PER_MINUTE_USD for your revenue rate.`,
59
+ // Corpus health (corpus moat metrics)
60
+ corpusPostmortems: corpusTotal,
61
+ topFailureModes: tagStats.slice(0, 5).map((s) => ({
62
+ tag: s.tag.replace(/^infra_/, ""),
63
+ incidents: s.count,
64
+ avgMttrLabel: s.avgMttrMs != null ? fmtMs(s.avgMttrMs) : null
65
+ })),
66
+ // Expansion signals (NRR growth)
67
+ servicesConnected: serviceSet.size,
68
+ services: [...serviceSet],
69
+ incidentsThisMonth
70
+ };
71
+ res.json({ ok: true, report });
72
+ });
73
+ return router;
74
+ }
75
+ export {
76
+ createBillingOutcomeRouter
77
+ };
@@ -0,0 +1,305 @@
1
+ import { Router } from "express";
2
+ import {
3
+ recordVerdict,
4
+ getStats,
5
+ getStatsForTag,
6
+ getRecords,
7
+ exportCsv,
8
+ getPendingFeedback,
9
+ getGlobalStats,
10
+ isCorpusSeeded,
11
+ getRealVerdictCount,
12
+ CALIBRATION_CONFIG
13
+ } from "../intelligence/calibration.js";
14
+ import { getClusters } from "../intelligence/unclassified-clusters.js";
15
+ import { getSessionMetrics } from "../intelligence/session-metrics.js";
16
+ import { computeRocCurve, getExecutionThreshold } from "../intelligence/threshold-optimizer.js";
17
+ import { computeBlastRadius } from "../intelligence/blast-radius.js";
18
+ import { plattScale, getPlattDiagnostics } from "../intelligence/platt-scaling.js";
19
+ import { getStores } from "../storage/store-registry.js";
20
+ const VALID_VERDICT_DIMENSIONS = /* @__PURE__ */ new Set(["root_cause", "fix_hint", "both"]);
21
+ function createCalibrationRouter() {
22
+ const router = Router();
23
+ router.post("/feedback", (req, res) => {
24
+ const { pid, verdict, note, verdictDimension } = req.body ?? {};
25
+ if (!pid || typeof pid !== "string") {
26
+ res.status(400).json({ ok: false, error: "pid (string) is required" });
27
+ return;
28
+ }
29
+ if (verdict !== "correct" && verdict !== "wrong" && verdict !== "partial") {
30
+ res.status(400).json({ ok: false, error: "verdict must be 'correct' | 'wrong' | 'partial'" });
31
+ return;
32
+ }
33
+ if (verdictDimension !== void 0 && !VALID_VERDICT_DIMENSIONS.has(verdictDimension)) {
34
+ res.status(400).json({
35
+ ok: false,
36
+ error: "verdictDimension must be 'root_cause' | 'fix_hint' | 'both'"
37
+ });
38
+ return;
39
+ }
40
+ const cleanNote = typeof note === "string" && note.trim() ? note : void 0;
41
+ const cleanDim = verdictDimension;
42
+ const result = recordVerdict(pid, verdict, cleanNote, cleanDim);
43
+ if (!result.found) {
44
+ res.status(404).json({ ok: false, error: `unknown pid: ${pid}` });
45
+ return;
46
+ }
47
+ if (!result.persisted) {
48
+ res.status(207).json({
49
+ ok: true,
50
+ warning: "verdict recorded in memory but failed to persist to disk",
51
+ retryable: true,
52
+ retryAfterMs: 5e3
53
+ });
54
+ return;
55
+ }
56
+ res.json({ ok: true });
57
+ });
58
+ router.get("/calibration", (_req, res) => {
59
+ const stats = getStats();
60
+ const trusted = stats.filter((s) => s.trusted);
61
+ const totalVerdicts = trusted.reduce((sum, s) => sum + s.verdicts, 0);
62
+ const overall = totalVerdicts > 0 ? trusted.reduce((sum, s) => sum + s.accuracy * s.verdicts, 0) / totalVerdicts : null;
63
+ const anyInterruptsAllowed = stats.some((s) => s.shouldInterrupt);
64
+ const corpusSeeded = isCorpusSeeded();
65
+ const realVerdictCount = getRealVerdictCount();
66
+ res.json({
67
+ ok: true,
68
+ overallAccuracy: overall,
69
+ trustedDetectors: trusted.length,
70
+ totalDetectors: stats.length,
71
+ anyInterruptsAllowed,
72
+ corpusSeeded,
73
+ realVerdictCount,
74
+ warmUpComplete: realVerdictCount >= 10,
75
+ pendingFeedback: getPendingFeedback(),
76
+ config: CALIBRATION_CONFIG,
77
+ perDetector: stats
78
+ });
79
+ });
80
+ router.get("/calibration/precision", (_req, res) => {
81
+ const stats = getStats();
82
+ const totalPredictions = stats.reduce((s, d) => s + d.predictions, 0);
83
+ const totalVerdicts = stats.reduce((s, d) => s + d.verdicts, 0);
84
+ const coverageRate = totalPredictions > 0 ? Math.round(totalVerdicts / totalPredictions * 100) / 100 : 0;
85
+ const trusted = stats.filter((d) => d.trusted);
86
+ const trustedVerdicts = trusted.reduce((s, d) => s + d.verdicts, 0);
87
+ const globalAccuracy = trustedVerdicts > 0 ? Math.round(trusted.reduce((s, d) => s + d.accuracy * d.verdicts, 0) / trustedVerdicts * 1e3) / 1e3 : null;
88
+ const detectors = stats.map((d) => ({
89
+ tag: d.tag,
90
+ predictions: d.predictions,
91
+ verdicts: d.verdicts,
92
+ precision: typeof d.accuracy === "number" ? Math.round(d.accuracy * 1e3) / 1e3 : null,
93
+ diagnosisPrecision: typeof d.diagnosisAccuracy === "number" ? Math.round(d.diagnosisAccuracy * 1e3) / 1e3 : null,
94
+ remediationPrecision: typeof d.remediationAccuracy === "number" ? Math.round(d.remediationAccuracy * 1e3) / 1e3 : null,
95
+ trusted: d.trusted,
96
+ trend: d.trendDelta !== null ? `${d.trendDelta >= 0 ? "+" : ""}${(d.trendDelta * 100).toFixed(1)}%` : null,
97
+ topFailureModes: d.commonFailureModes.map((f) => f.note)
98
+ }));
99
+ res.json({
100
+ ok: true,
101
+ generated: (/* @__PURE__ */ new Date()).toISOString(),
102
+ note: "Precision = P(correct | verdict given). Recall not measurable without ground-truth negatives.",
103
+ totalPredictions,
104
+ totalVerdicts,
105
+ coverageRate,
106
+ globalAccuracy,
107
+ detectors
108
+ });
109
+ });
110
+ router.get("/calibration/export", (_req, res) => {
111
+ const csv = exportCsv();
112
+ res.setHeader("Content-Type", "text/csv; charset=utf-8");
113
+ res.setHeader(
114
+ "Content-Disposition",
115
+ `attachment; filename="mergen-calibration-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv"`
116
+ );
117
+ res.send(csv);
118
+ });
119
+ router.get("/calibration/unclassified", (req, res) => {
120
+ const minCount = Math.max(1, parseInt(String(req.query["minCount"] ?? "3"), 10) || 3);
121
+ const clusters = getClusters(minCount);
122
+ res.json({
123
+ ok: true,
124
+ total: clusters.length,
125
+ minCount,
126
+ clusters
127
+ });
128
+ });
129
+ router.get("/calibration/global", (_req, res) => {
130
+ res.json({
131
+ ok: true,
132
+ stats: getGlobalStats()
133
+ });
134
+ });
135
+ router.get("/calibration/corpus-progress", (_req, res) => {
136
+ const TARGET = 20;
137
+ const records = getRecords().filter((r) => !r.isBuiltinSeed);
138
+ const highCorrect = records.filter(
139
+ (r) => r.confidence === "HIGH" && (r.verdict === "correct" || r.verdict === "partial")
140
+ ).length;
141
+ const corpusSeeded = isCorpusSeeded();
142
+ const trustedDetectors = corpusSeeded ? 0 : getStats().filter((s) => s.trusted).length;
143
+ res.json({
144
+ ok: true,
145
+ highConfidentCorrect: highCorrect,
146
+ target: TARGET,
147
+ targetReached: highCorrect >= TARGET,
148
+ pct: Math.min(100, Math.round(highCorrect / TARGET * 100)),
149
+ trustedDetectors,
150
+ totalVerdicts: records.filter((r) => r.verdict).length,
151
+ corpusSeeded
152
+ });
153
+ });
154
+ router.get("/session-metrics", (_req, res) => {
155
+ res.json({ ok: true, ...getSessionMetrics() });
156
+ });
157
+ router.get("/calibration/threshold", (_req, res) => {
158
+ const records = getRecords();
159
+ const verdicted = records.filter((r) => r.verdict !== void 0);
160
+ const withNumeric = verdicted.filter((r) => r.numericScore !== void 0);
161
+ const rocCurve = computeRocCurve();
162
+ const recommended = getExecutionThreshold();
163
+ res.json({
164
+ ok: true,
165
+ generated: (/* @__PURE__ */ new Date()).toISOString(),
166
+ note: "Threshold maximizes Youden's J (TPR - FPR). Min 20 verdicts required; uses 0.85 fallback until then.",
167
+ currentThreshold: 0.85,
168
+ recommendedThreshold: recommended,
169
+ usingFallback: rocCurve.length === 0,
170
+ sampleSize: verdicted.length,
171
+ sampleSizeWithNumericScore: withNumeric.length,
172
+ rocCurve
173
+ });
174
+ });
175
+ router.get("/blast-radius", (req, res) => {
176
+ const command = typeof req.query.command === "string" ? req.query.command.trim() : "";
177
+ if (!command) {
178
+ res.status(400).json({ ok: false, error: "command query param is required" });
179
+ return;
180
+ }
181
+ const service = typeof req.query.service === "string" ? req.query.service : void 0;
182
+ const namespace = typeof req.query.namespace === "string" ? req.query.namespace : void 0;
183
+ const environment = typeof req.query.environment === "string" ? req.query.environment : void 0;
184
+ const br = computeBlastRadius(command, { service, namespace, environment });
185
+ res.json({ ok: true, ...br });
186
+ });
187
+ router.get("/trust-score/:pid", async (req, res) => {
188
+ const { pid } = req.params;
189
+ const inc = await getStores().incidents.get(pid, req.tenantId);
190
+ if (!inc) {
191
+ res.status(404).json({ error: "incident not found", pid });
192
+ return;
193
+ }
194
+ await getStores().incidents.markContextViewed(pid, req.tenantId);
195
+ const rawScore = inc.confidence;
196
+ const tag = inc.tag;
197
+ const result = plattScale(rawScore, tag);
198
+ const pct = Math.round(result.calibrated * 100);
199
+ const records = getRecords().filter((r) => r["pid"] === pid);
200
+ const verdictHistory = records.map((r) => ({
201
+ verdict: r["verdict"] ?? null,
202
+ note: r["note"] ?? null,
203
+ recordedAt: r["recordedAt"] ?? null,
204
+ dimension: r["verdictDimension"] ?? null
205
+ }));
206
+ const tagStats = getStatsForTag(tag);
207
+ const interpretation = pct >= 85 ? "high \u2014 strong historical basis for automated action" : pct >= 65 ? "medium \u2014 recommend human review before execution" : pct >= 40 ? "low \u2014 diagnosis is a signal, not a conclusion" : "insufficient \u2014 surface as context only";
208
+ res.json({
209
+ ok: true,
210
+ pid,
211
+ service: inc.service ?? null,
212
+ tag,
213
+ rawScore,
214
+ calibrated: result.calibrated,
215
+ calibratedPct: pct,
216
+ calibrationSource: result.source,
217
+ calibrationSampleSize: result.n,
218
+ verdictHistory,
219
+ tagAccuracy: tagStats?.accuracy ?? null,
220
+ tagSampleSize: tagStats?.n ?? null,
221
+ interpretation,
222
+ resolvedAutonomously: inc.resolvedAutonomously,
223
+ causallyCorrect: inc.causallyCorrect
224
+ });
225
+ });
226
+ router.get("/trust-score", (req, res) => {
227
+ const tag = typeof req.query.tag === "string" ? req.query.tag : void 0;
228
+ const rawScore = parseFloat(String(req.query.rawScore ?? req.query.score ?? ""));
229
+ if (isNaN(rawScore) || rawScore < 0 || rawScore > 1) {
230
+ res.status(400).json({ error: "rawScore must be a number between 0 and 1" });
231
+ return;
232
+ }
233
+ const result = plattScale(rawScore, tag);
234
+ const pct = Math.round(result.calibrated * 100);
235
+ const interpretation = pct >= 85 ? "high \u2014 strong historical basis for automated action" : pct >= 65 ? "medium \u2014 recommend human review before execution" : pct >= 40 ? "low \u2014 diagnosis is a signal, not a conclusion" : "insufficient \u2014 surface as context only";
236
+ res.json({
237
+ ok: true,
238
+ rawScore,
239
+ tag: tag ?? null,
240
+ calibrated: result.calibrated,
241
+ calibratedPct: pct,
242
+ source: result.source,
243
+ empiricalBasis: result.n,
244
+ interpretation,
245
+ // Diagnostic: show all fitted Platt models (useful for PoC demos)
246
+ models: tag ? void 0 : getPlattDiagnostics()
247
+ });
248
+ });
249
+ router.get("/confidence-report", async (req, res) => {
250
+ const windowDays = Math.min(90, Math.max(1, parseInt(String(req.query.days ?? "30"), 10)));
251
+ const windowStart = Date.now() - windowDays * 24 * 60 * 60 * 1e3;
252
+ const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1e3;
253
+ const currentThreshold = getExecutionThreshold();
254
+ const roc = computeRocCurve();
255
+ const bestRoc = roc.length > 0 ? roc.reduce((best, pt) => pt.youdensJ > best.youdensJ ? pt : best, roc[0]) : null;
256
+ const records = getRecords();
257
+ const thresholdSampleSize = records.filter((r) => r.verdict !== null).length;
258
+ const allIncidents = await getStores().incidents.list(void 0, 1e4, req.tenantId);
259
+ const windowIncidents = allIncidents.filter(
260
+ (i) => (i.createdAt ?? 0) >= windowStart
261
+ );
262
+ const autonomous = windowIncidents.filter((i) => i.resolvedAutonomously);
263
+ const successful = autonomous.filter((i) => i.causallyCorrect === true);
264
+ const falsePositives = autonomous.filter((i) => i.causallyCorrect === false);
265
+ const recent7d = autonomous.filter((i) => (i.resolvedAt ?? i.createdAt ?? 0) >= sevenDaysAgo);
266
+ const recent7dSuccess = recent7d.filter((i) => i.causallyCorrect === true);
267
+ const rollingRate7d = recent7d.length > 0 ? Math.round(recent7dSuccess.length / recent7d.length * 1e3) / 1e3 : null;
268
+ const stats = getStats();
269
+ const detectorHealth = stats.map((s) => ({
270
+ tag: s.tag,
271
+ accuracy: Math.round(s.accuracy * 1e3) / 1e3,
272
+ verdicts: s.verdicts,
273
+ trusted: s.trusted,
274
+ status: s.accuracy >= 0.8 ? "healthy" : s.accuracy >= 0.6 ? "degraded" : "poor"
275
+ })).sort((a, b) => b.verdicts - a.verdicts);
276
+ const successRate = autonomous.length > 0 ? Math.round(successful.length / autonomous.length * 100) : null;
277
+ const deckSummary = autonomous.length === 0 ? `No autonomous resolutions yet in the last ${windowDays} days. Mergen is in observation mode.` : `Mergen acted autonomously ${autonomous.length} time${autonomous.length !== 1 ? "s" : ""} in ${windowDays} days. Success rate: ${successRate}%. Current confidence threshold: ${Math.round(currentThreshold * 100)}%` + (thresholdSampleSize >= 10 ? ` (calibrated from ${thresholdSampleSize} verdicts).` : " (default prior \u2014 calibrates with use).");
278
+ res.json({
279
+ ok: true,
280
+ report: {
281
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
282
+ windowDays,
283
+ threshold: {
284
+ current: Math.round(currentThreshold * 1e3) / 1e3,
285
+ recommendation: bestRoc ? Math.round(bestRoc.threshold * 1e3) / 1e3 : Math.round(currentThreshold * 1e3) / 1e3,
286
+ basis: thresholdSampleSize >= 10 ? "empirical" : "prior",
287
+ sampleSize: thresholdSampleSize
288
+ },
289
+ autonomousAccuracy: {
290
+ attempts: autonomous.length,
291
+ successful: successful.length,
292
+ falsePositives: falsePositives.length,
293
+ rollingRate7d,
294
+ label: successRate !== null ? `${successRate}% of autonomous fixes resolved the incident` : "No autonomous actions yet \u2014 enable MERGEN_AUTOPILOT=true"
295
+ },
296
+ detectorHealth,
297
+ deckSummary
298
+ }
299
+ });
300
+ });
301
+ return router;
302
+ }
303
+ export {
304
+ createCalibrationRouter
305
+ };
@@ -0,0 +1,20 @@
1
+ import { Router } from "express";
2
+ import { getStores } from "../storage/store-registry.js";
3
+ function createCIGateHistoryRouter() {
4
+ const router = Router();
5
+ router.get("/ci/gate/history", async (req, res) => {
6
+ const prRaw = req.query.pr;
7
+ const pr = typeof prRaw === "string" ? parseInt(prRaw, 10) : NaN;
8
+ if (!Number.isFinite(pr)) {
9
+ res.status(400).json({ ok: false, error: 'query param "pr" is required and must be numeric' });
10
+ return;
11
+ }
12
+ const repo = typeof req.query.repo === "string" ? req.query.repo : void 0;
13
+ const entries = await getStores().gateHistory.listForPR(pr, repo, req.tenantId);
14
+ res.json({ ok: true, pr, entries });
15
+ });
16
+ return router;
17
+ }
18
+ export {
19
+ createCIGateHistoryRouter
20
+ };