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,133 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { DATA_DIR } from "./paths.js";
|
|
3
|
+
import logger from "./logger.js";
|
|
4
|
+
const HEARTBEAT_FILE = `${DATA_DIR}/heartbeats.json`;
|
|
5
|
+
const CHECK_INTERVAL_MS = 6e4;
|
|
6
|
+
let _state = {};
|
|
7
|
+
let _loaded = false;
|
|
8
|
+
function load() {
|
|
9
|
+
if (_loaded) return;
|
|
10
|
+
_loaded = true;
|
|
11
|
+
if (!fs.existsSync(HEARTBEAT_FILE)) return;
|
|
12
|
+
try {
|
|
13
|
+
const raw = JSON.parse(fs.readFileSync(HEARTBEAT_FILE, "utf8"));
|
|
14
|
+
if (raw?.version === 1 && raw.heartbeats) _state = raw.heartbeats;
|
|
15
|
+
} catch (err) {
|
|
16
|
+
logger.warn({ err }, "heartbeat-monitor: failed to load state \u2014 starting fresh");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
let _tmpCounter = 0;
|
|
20
|
+
function persist() {
|
|
21
|
+
try {
|
|
22
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
23
|
+
const payload = { version: 1, heartbeats: _state };
|
|
24
|
+
_tmpCounter = _tmpCounter + 1 >>> 0;
|
|
25
|
+
const tmp = `${HEARTBEAT_FILE}.tmp.${process.pid}.${Date.now().toString(36)}.${_tmpCounter}`;
|
|
26
|
+
fs.writeFileSync(tmp, JSON.stringify(payload), "utf8");
|
|
27
|
+
fs.renameSync(tmp, HEARTBEAT_FILE);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
logger.warn({ err }, "heartbeat-monitor: persist failed");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function computeStatus(hb) {
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
const deadline = (hb.intervalSeconds + hb.graceSeconds) * 1e3;
|
|
35
|
+
if (!hb.lastPingAt) {
|
|
36
|
+
const age = now - hb.createdAt;
|
|
37
|
+
return {
|
|
38
|
+
...hb,
|
|
39
|
+
status: age > deadline ? "never-pinged" : "ok",
|
|
40
|
+
secondsSinceLastPing: null,
|
|
41
|
+
nextExpectedAt: hb.createdAt + deadline,
|
|
42
|
+
overdueSeconds: age > deadline ? Math.round((age - deadline) / 1e3) : null
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const elapsed = now - hb.lastPingAt;
|
|
46
|
+
const isLate = elapsed > hb.intervalSeconds * 1e3;
|
|
47
|
+
const isMissing = elapsed > deadline;
|
|
48
|
+
return {
|
|
49
|
+
...hb,
|
|
50
|
+
status: isMissing ? "missing" : isLate ? "late" : "ok",
|
|
51
|
+
secondsSinceLastPing: Math.round(elapsed / 1e3),
|
|
52
|
+
nextExpectedAt: hb.lastPingAt + hb.intervalSeconds * 1e3,
|
|
53
|
+
overdueSeconds: isMissing ? Math.round((elapsed - deadline) / 1e3) : null
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
let _alertFn = null;
|
|
57
|
+
function setHeartbeatAlertFn(fn) {
|
|
58
|
+
_alertFn = fn;
|
|
59
|
+
}
|
|
60
|
+
function checkHeartbeats() {
|
|
61
|
+
load();
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
for (const hb of Object.values(_state)) {
|
|
64
|
+
const report = computeStatus(hb);
|
|
65
|
+
if (report.status !== "missing" && report.status !== "never-pinged") continue;
|
|
66
|
+
const alertCooldown = hb.intervalSeconds * 1e3;
|
|
67
|
+
if (hb.lastAlertedAt && now - hb.lastAlertedAt < alertCooldown) continue;
|
|
68
|
+
const msg = report.status === "never-pinged" ? `Heartbeat \`${hb.name}\` has never received a ping \u2014 expected within ${hb.intervalSeconds}s of registration.` : `Heartbeat \`${hb.name}\` missed \u2014 ${report.overdueSeconds}s overdue (interval: ${hb.intervalSeconds}s, grace: ${hb.graceSeconds}s).`;
|
|
69
|
+
logger.warn({ name: hb.name, status: report.status, overdueSeconds: report.overdueSeconds }, "heartbeat-monitor: missed heartbeat");
|
|
70
|
+
if (_alertFn) _alertFn(hb.name, hb.description ? `${msg} \u2014 ${hb.description}` : msg);
|
|
71
|
+
_state[hb.name] = { ...hb, lastAlertedAt: now };
|
|
72
|
+
persist();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
let _checkHandle = null;
|
|
76
|
+
function startHeartbeatMonitor() {
|
|
77
|
+
_checkHandle = setInterval(checkHeartbeats, CHECK_INTERVAL_MS);
|
|
78
|
+
_checkHandle.unref();
|
|
79
|
+
logger.info("heartbeat-monitor: started");
|
|
80
|
+
return () => {
|
|
81
|
+
if (_checkHandle) {
|
|
82
|
+
clearInterval(_checkHandle);
|
|
83
|
+
_checkHandle = null;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function ping(name, intervalSeconds = 86400, graceSeconds, description) {
|
|
88
|
+
load();
|
|
89
|
+
const grace = graceSeconds ?? Math.max(60, Math.round(intervalSeconds * 0.1));
|
|
90
|
+
const existing = _state[name];
|
|
91
|
+
const updated = {
|
|
92
|
+
name,
|
|
93
|
+
intervalSeconds,
|
|
94
|
+
graceSeconds: grace,
|
|
95
|
+
createdAt: existing?.createdAt ?? Date.now(),
|
|
96
|
+
lastPingAt: Date.now(),
|
|
97
|
+
pingCount: (existing?.pingCount ?? 0) + 1,
|
|
98
|
+
lastAlertedAt: existing?.lastAlertedAt ?? null,
|
|
99
|
+
description: description ?? existing?.description
|
|
100
|
+
};
|
|
101
|
+
_state[name] = updated;
|
|
102
|
+
persist();
|
|
103
|
+
logger.debug({ name, pingCount: updated.pingCount }, "heartbeat-monitor: ping received");
|
|
104
|
+
return updated;
|
|
105
|
+
}
|
|
106
|
+
function getReport(name) {
|
|
107
|
+
load();
|
|
108
|
+
const hb = _state[name];
|
|
109
|
+
if (!hb) return null;
|
|
110
|
+
return computeStatus(hb);
|
|
111
|
+
}
|
|
112
|
+
function getAllReports() {
|
|
113
|
+
load();
|
|
114
|
+
return Object.values(_state).map(computeStatus).sort((a, b) => {
|
|
115
|
+
const order = { missing: 0, "never-pinged": 1, late: 2, ok: 3 };
|
|
116
|
+
return order[a.status] - order[b.status] || a.name.localeCompare(b.name);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function removeHeartbeat(name) {
|
|
120
|
+
load();
|
|
121
|
+
if (!_state[name]) return false;
|
|
122
|
+
delete _state[name];
|
|
123
|
+
persist();
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
export {
|
|
127
|
+
getAllReports,
|
|
128
|
+
getReport,
|
|
129
|
+
ping,
|
|
130
|
+
removeHeartbeat,
|
|
131
|
+
setHeartbeatAlertFn,
|
|
132
|
+
startHeartbeatMonitor
|
|
133
|
+
};
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import initSqlJs from "sql.js";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { createRequire } from "module";
|
|
6
|
+
import { DATA_DIR } from "./paths.js";
|
|
7
|
+
import logger from "./logger.js";
|
|
8
|
+
const INCIDENT_DB = path.join(DATA_DIR, "incidents.db");
|
|
9
|
+
class IncidentStore {
|
|
10
|
+
db = null;
|
|
11
|
+
resolveWasmPath() {
|
|
12
|
+
if (process.env.MERGEN_WASM_PATH) return process.env.MERGEN_WASM_PATH;
|
|
13
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const fromModule = path.resolve(moduleDir, "../../node_modules/sql.js/dist/sql-wasm.wasm");
|
|
15
|
+
if (fs.existsSync(fromModule)) return fromModule;
|
|
16
|
+
try {
|
|
17
|
+
const req = createRequire(import.meta.url);
|
|
18
|
+
const resolved = path.join(path.dirname(req.resolve("sql.js")), "sql-wasm.wasm");
|
|
19
|
+
if (fs.existsSync(resolved)) return resolved;
|
|
20
|
+
} catch {
|
|
21
|
+
}
|
|
22
|
+
return fromModule;
|
|
23
|
+
}
|
|
24
|
+
async init() {
|
|
25
|
+
try {
|
|
26
|
+
const wasmBinary = fs.readFileSync(this.resolveWasmPath());
|
|
27
|
+
const SQL = await initSqlJs({ wasmBinary });
|
|
28
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
29
|
+
let fileBuffer;
|
|
30
|
+
if (fs.existsSync(INCIDENT_DB)) {
|
|
31
|
+
try {
|
|
32
|
+
fileBuffer = fs.readFileSync(INCIDENT_DB);
|
|
33
|
+
} catch {
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
this.db = fileBuffer ? new SQL.Database(fileBuffer) : new SQL.Database();
|
|
37
|
+
this.db.run(`
|
|
38
|
+
CREATE TABLE IF NOT EXISTS incidents (
|
|
39
|
+
pid TEXT PRIMARY KEY,
|
|
40
|
+
hypothesis TEXT NOT NULL,
|
|
41
|
+
tag TEXT NOT NULL DEFAULT '',
|
|
42
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
43
|
+
assignee TEXT,
|
|
44
|
+
notes TEXT NOT NULL DEFAULT '[]',
|
|
45
|
+
sha TEXT,
|
|
46
|
+
environment TEXT,
|
|
47
|
+
service TEXT,
|
|
48
|
+
cluster TEXT,
|
|
49
|
+
confidence REAL NOT NULL DEFAULT 0,
|
|
50
|
+
created_at INTEGER NOT NULL,
|
|
51
|
+
updated_at INTEGER NOT NULL,
|
|
52
|
+
acknowledged_by TEXT,
|
|
53
|
+
resolved_at INTEGER,
|
|
54
|
+
resolved_autonomously INTEGER NOT NULL DEFAULT 0,
|
|
55
|
+
causally_correct INTEGER NOT NULL DEFAULT 0
|
|
56
|
+
);
|
|
57
|
+
`);
|
|
58
|
+
this.db.run(`
|
|
59
|
+
CREATE TABLE IF NOT EXISTS service_edges (
|
|
60
|
+
source TEXT NOT NULL,
|
|
61
|
+
target TEXT NOT NULL,
|
|
62
|
+
weight INTEGER NOT NULL DEFAULT 1,
|
|
63
|
+
last_incident_at INTEGER NOT NULL,
|
|
64
|
+
PRIMARY KEY (source, target)
|
|
65
|
+
);
|
|
66
|
+
`);
|
|
67
|
+
try {
|
|
68
|
+
this.db.run(`ALTER TABLE incidents ADD COLUMN service TEXT`);
|
|
69
|
+
} catch {
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
this.db.run(`ALTER TABLE incidents ADD COLUMN cluster TEXT`);
|
|
73
|
+
} catch {
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
this.db.run(`ALTER TABLE incidents ADD COLUMN causally_correct INTEGER NOT NULL DEFAULT 0`);
|
|
77
|
+
} catch {
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
this.db.run(`ALTER TABLE incidents ADD COLUMN context_brief_viewed_at INTEGER`);
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
this._flush();
|
|
84
|
+
logger.info({ path: INCIDENT_DB }, "incident store initialised");
|
|
85
|
+
} catch (err) {
|
|
86
|
+
logger.warn({ err }, "incident store failed to init \u2014 running without persistence");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
_flush() {
|
|
90
|
+
if (!this.db) return;
|
|
91
|
+
try {
|
|
92
|
+
const data = this.db.export();
|
|
93
|
+
fs.writeFileSync(INCIDENT_DB, Buffer.from(data));
|
|
94
|
+
} catch (err) {
|
|
95
|
+
logger.warn({ err }, "incident store flush failed");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
_row(row) {
|
|
99
|
+
return {
|
|
100
|
+
pid: String(row.pid ?? ""),
|
|
101
|
+
hypothesis: String(row.hypothesis ?? ""),
|
|
102
|
+
tag: String(row.tag ?? ""),
|
|
103
|
+
status: row.status ?? "open",
|
|
104
|
+
assignee: row.assignee ? String(row.assignee) : null,
|
|
105
|
+
notes: (() => {
|
|
106
|
+
try {
|
|
107
|
+
return JSON.parse(String(row.notes ?? "[]"));
|
|
108
|
+
} catch {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
})(),
|
|
112
|
+
sha: row.sha ? String(row.sha) : null,
|
|
113
|
+
environment: row.environment ? String(row.environment) : null,
|
|
114
|
+
service: row.service ? String(row.service) : null,
|
|
115
|
+
cluster: row.cluster ? String(row.cluster) : null,
|
|
116
|
+
confidence: Number(row.confidence ?? 0),
|
|
117
|
+
createdAt: Number(row.created_at ?? 0),
|
|
118
|
+
updatedAt: Number(row.updated_at ?? 0),
|
|
119
|
+
acknowledgedBy: row.acknowledged_by ? String(row.acknowledged_by) : null,
|
|
120
|
+
resolvedAt: row.resolved_at ? Number(row.resolved_at) : null,
|
|
121
|
+
resolvedAutonomously: Boolean(row.resolved_autonomously),
|
|
122
|
+
causallyCorrect: Boolean(row.causally_correct),
|
|
123
|
+
contextBriefViewedAt: row.context_brief_viewed_at ? Number(row.context_brief_viewed_at) : null
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
upsert(pid, fields) {
|
|
127
|
+
if (!this.db) {
|
|
128
|
+
const now2 = Date.now();
|
|
129
|
+
return {
|
|
130
|
+
pid,
|
|
131
|
+
status: "open",
|
|
132
|
+
hypothesis: "",
|
|
133
|
+
tag: "",
|
|
134
|
+
assignee: null,
|
|
135
|
+
notes: [],
|
|
136
|
+
sha: null,
|
|
137
|
+
environment: null,
|
|
138
|
+
service: null,
|
|
139
|
+
cluster: null,
|
|
140
|
+
confidence: 0,
|
|
141
|
+
createdAt: now2,
|
|
142
|
+
updatedAt: now2,
|
|
143
|
+
acknowledgedBy: null,
|
|
144
|
+
resolvedAt: null,
|
|
145
|
+
resolvedAutonomously: false,
|
|
146
|
+
causallyCorrect: false,
|
|
147
|
+
contextBriefViewedAt: null,
|
|
148
|
+
...fields
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const existing = this.get(pid);
|
|
152
|
+
const now = Date.now();
|
|
153
|
+
if (!existing) {
|
|
154
|
+
this.db.run(
|
|
155
|
+
`INSERT INTO incidents (pid, hypothesis, tag, status, assignee, notes, sha, environment, service, cluster, confidence, created_at, updated_at, acknowledged_by, resolved_at, resolved_autonomously, causally_correct)
|
|
156
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
|
157
|
+
[
|
|
158
|
+
pid,
|
|
159
|
+
fields.hypothesis ?? "",
|
|
160
|
+
fields.tag ?? "",
|
|
161
|
+
fields.status ?? "open",
|
|
162
|
+
fields.assignee ?? null,
|
|
163
|
+
JSON.stringify(fields.notes ?? []),
|
|
164
|
+
fields.sha ?? null,
|
|
165
|
+
fields.environment ?? null,
|
|
166
|
+
fields.service ?? null,
|
|
167
|
+
fields.cluster ?? null,
|
|
168
|
+
fields.confidence ?? 0,
|
|
169
|
+
now,
|
|
170
|
+
now,
|
|
171
|
+
fields.acknowledgedBy ?? null,
|
|
172
|
+
fields.resolvedAt ?? null,
|
|
173
|
+
fields.resolvedAutonomously ? 1 : 0,
|
|
174
|
+
fields.causallyCorrect ? 1 : 0
|
|
175
|
+
]
|
|
176
|
+
);
|
|
177
|
+
} else {
|
|
178
|
+
const merged = { ...existing, ...fields };
|
|
179
|
+
this.db.run(
|
|
180
|
+
`UPDATE incidents SET hypothesis=?,tag=?,status=?,assignee=?,notes=?,sha=?,environment=?,service=?,cluster=?,confidence=?,updated_at=?,acknowledged_by=?,resolved_at=?,resolved_autonomously=?,causally_correct=? WHERE pid=?`,
|
|
181
|
+
[
|
|
182
|
+
merged.hypothesis,
|
|
183
|
+
merged.tag,
|
|
184
|
+
merged.status,
|
|
185
|
+
merged.assignee,
|
|
186
|
+
JSON.stringify(merged.notes),
|
|
187
|
+
merged.sha,
|
|
188
|
+
merged.environment,
|
|
189
|
+
merged.service,
|
|
190
|
+
merged.cluster,
|
|
191
|
+
merged.confidence,
|
|
192
|
+
now,
|
|
193
|
+
merged.acknowledgedBy,
|
|
194
|
+
merged.resolvedAt,
|
|
195
|
+
merged.resolvedAutonomously ? 1 : 0,
|
|
196
|
+
merged.causallyCorrect ? 1 : 0,
|
|
197
|
+
pid
|
|
198
|
+
]
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
this._flush();
|
|
202
|
+
const result = this.get(pid);
|
|
203
|
+
if (result.service) {
|
|
204
|
+
this.updateServiceEdges(result.service, result.updatedAt);
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
get(pid) {
|
|
209
|
+
if (!this.db) return null;
|
|
210
|
+
try {
|
|
211
|
+
const res = this.db.exec("SELECT * FROM incidents WHERE pid=?", [pid]);
|
|
212
|
+
if (!res[0]?.values?.length) return null;
|
|
213
|
+
const cols = res[0].columns;
|
|
214
|
+
const vals = res[0].values[0];
|
|
215
|
+
const row = {};
|
|
216
|
+
cols.forEach((c, i) => {
|
|
217
|
+
row[c] = vals[i];
|
|
218
|
+
});
|
|
219
|
+
return this._row(row);
|
|
220
|
+
} catch {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
list(status, limit = 50) {
|
|
225
|
+
if (!this.db) return [];
|
|
226
|
+
try {
|
|
227
|
+
const sql = status ? `SELECT * FROM incidents WHERE status=? ORDER BY updated_at DESC LIMIT ?` : `SELECT * FROM incidents ORDER BY updated_at DESC LIMIT ?`;
|
|
228
|
+
const params = status ? [status, limit] : [limit];
|
|
229
|
+
const res = this.db.exec(sql, params);
|
|
230
|
+
if (!res[0]?.values) return [];
|
|
231
|
+
return res[0].values.map((vals) => {
|
|
232
|
+
const row = {};
|
|
233
|
+
res[0].columns.forEach((c, i) => {
|
|
234
|
+
row[c] = vals[i];
|
|
235
|
+
});
|
|
236
|
+
return this._row(row);
|
|
237
|
+
});
|
|
238
|
+
} catch {
|
|
239
|
+
return [];
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
markContextViewed(pid) {
|
|
243
|
+
if (!this.db) return;
|
|
244
|
+
const existing = this.get(pid);
|
|
245
|
+
if (!existing || existing.contextBriefViewedAt != null) return;
|
|
246
|
+
try {
|
|
247
|
+
this.db.run(
|
|
248
|
+
`UPDATE incidents SET context_brief_viewed_at=? WHERE pid=?`,
|
|
249
|
+
[Date.now(), pid]
|
|
250
|
+
);
|
|
251
|
+
this._flush();
|
|
252
|
+
} catch (err) {
|
|
253
|
+
logger.warn({ err, pid }, "incident store: markContextViewed failed");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Co-occurring services: other services that had incidents within windowMs of
|
|
258
|
+
* any incident for the given service. Pure SQL — no JS O(n²) scan.
|
|
259
|
+
* Returns up to `limit` services sorted by co-occurrence count descending.
|
|
260
|
+
*/
|
|
261
|
+
coOccurringServices(service, windowMs = 10 * 60 * 1e3, limit = 4) {
|
|
262
|
+
if (!this.db) return [];
|
|
263
|
+
try {
|
|
264
|
+
const res = this.db.exec(
|
|
265
|
+
`SELECT other.service, COUNT(*) AS cnt
|
|
266
|
+
FROM incidents AS base
|
|
267
|
+
JOIN incidents AS other
|
|
268
|
+
ON other.service != base.service
|
|
269
|
+
AND other.service IS NOT NULL
|
|
270
|
+
AND other.created_at BETWEEN base.created_at - ? AND base.created_at + ?
|
|
271
|
+
WHERE base.service = ?
|
|
272
|
+
GROUP BY other.service
|
|
273
|
+
ORDER BY cnt DESC
|
|
274
|
+
LIMIT ?`,
|
|
275
|
+
[windowMs, windowMs, service, limit]
|
|
276
|
+
);
|
|
277
|
+
if (!res[0]?.values) return [];
|
|
278
|
+
return res[0].values.map((v) => ({
|
|
279
|
+
service: String(v[0] ?? ""),
|
|
280
|
+
count: Number(v[1] ?? 0)
|
|
281
|
+
}));
|
|
282
|
+
} catch {
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
addNote(pid, note, author) {
|
|
287
|
+
const inc = this.get(pid);
|
|
288
|
+
if (!inc) return null;
|
|
289
|
+
const entry = author ? `[${author}] ${note}` : note;
|
|
290
|
+
return this.upsert(pid, { notes: [...inc.notes, entry] });
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Persist a co-occurrence edge between `service` and every other service
|
|
294
|
+
* that had an incident within windowMs. Called on every upsert so the
|
|
295
|
+
* graph accumulates weight over time without a separate indexing job.
|
|
296
|
+
*/
|
|
297
|
+
updateServiceEdges(service, at, windowMs = 10 * 60 * 1e3) {
|
|
298
|
+
if (!this.db) return;
|
|
299
|
+
try {
|
|
300
|
+
const coServices = this.coOccurringServices(service, windowMs, 20);
|
|
301
|
+
for (const { service: target } of coServices) {
|
|
302
|
+
for (const [src, tgt] of [[service, target], [target, service]]) {
|
|
303
|
+
this.db.run(
|
|
304
|
+
`INSERT INTO service_edges (source, target, weight, last_incident_at) VALUES (?,?,1,?)
|
|
305
|
+
ON CONFLICT(source, target) DO UPDATE SET weight = weight + 1, last_incident_at = excluded.last_incident_at`,
|
|
306
|
+
[src, tgt, at]
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
this._flush();
|
|
311
|
+
} catch (err) {
|
|
312
|
+
logger.warn({ err, service }, "incident store: updateServiceEdges failed");
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Return the full interaction graph, optionally filtered to edges touching `service`.
|
|
317
|
+
* Sorted by weight descending — strongest co-occurrence relationships first.
|
|
318
|
+
*/
|
|
319
|
+
getInteractionGraph(service) {
|
|
320
|
+
if (!this.db) return [];
|
|
321
|
+
try {
|
|
322
|
+
const sql = service ? `SELECT source, target, weight, last_incident_at FROM service_edges WHERE source=? OR target=? ORDER BY weight DESC LIMIT 100` : `SELECT source, target, weight, last_incident_at FROM service_edges ORDER BY weight DESC LIMIT 200`;
|
|
323
|
+
const params = service ? [service, service] : [];
|
|
324
|
+
const res = this.db.exec(sql, params);
|
|
325
|
+
if (!res[0]?.values) return [];
|
|
326
|
+
return res[0].values.map((v) => ({
|
|
327
|
+
source: String(v[0] ?? ""),
|
|
328
|
+
target: String(v[1] ?? ""),
|
|
329
|
+
weight: Number(v[2] ?? 0),
|
|
330
|
+
lastIncidentAt: Number(v[3] ?? 0)
|
|
331
|
+
}));
|
|
332
|
+
} catch {
|
|
333
|
+
return [];
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const incidentStore = new IncidentStore();
|
|
338
|
+
export {
|
|
339
|
+
incidentStore
|
|
340
|
+
};
|