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,107 @@
1
+ import { getSql } from "./pg-client.js";
2
+ const DEFAULT_TENANT = "local";
3
+ function rowToPending(row) {
4
+ return {
5
+ pid: String(row.pid ?? ""),
6
+ command: String(row.command ?? ""),
7
+ tier: row.tier ?? "restart",
8
+ service: String(row.service ?? ""),
9
+ remediationConfidence: Number(row.remediation_confidence ?? 0),
10
+ requestedAt: row.requested_at instanceof Date ? row.requested_at.getTime() : new Date(String(row.requested_at ?? 0)).getTime(),
11
+ expiresAt: row.expires_at instanceof Date ? row.expires_at.getTime() : new Date(String(row.expires_at ?? 0)).getTime(),
12
+ cwd: row.cwd ? String(row.cwd) : void 0,
13
+ blastRadius: row.blast_radius ? row.blast_radius : void 0
14
+ };
15
+ }
16
+ class PgApprovalStore {
17
+ async add(token, execution, tenantId = DEFAULT_TENANT) {
18
+ const sql = getSql();
19
+ const lastRows = await sql`
20
+ SELECT hash FROM pending_approvals
21
+ WHERE tenant_id = ${tenantId}
22
+ ORDER BY requested_at DESC, token DESC
23
+ LIMIT 1
24
+ `;
25
+ const prevHash = lastRows.length > 0 && lastRows[0].hash ? String(lastRows[0].hash) : "genesis_approvals";
26
+ const crypto = await import("crypto");
27
+ const payload = [
28
+ token,
29
+ execution.pid,
30
+ execution.command,
31
+ execution.tier,
32
+ execution.service,
33
+ String(execution.remediationConfidence),
34
+ String(execution.requestedAt)
35
+ ].join("|");
36
+ const hash = crypto.createHash("sha256").update(payload + prevHash).digest("hex");
37
+ await sql`
38
+ INSERT INTO pending_approvals (
39
+ token, tenant_id, pid, command, tier, service,
40
+ remediation_confidence, requested_at, expires_at, cwd, blast_radius,
41
+ hash, prev_hash
42
+ ) VALUES (
43
+ ${token},
44
+ ${tenantId},
45
+ ${execution.pid},
46
+ ${execution.command},
47
+ ${execution.tier},
48
+ ${execution.service},
49
+ ${execution.remediationConfidence},
50
+ ${new Date(execution.requestedAt)},
51
+ ${new Date(execution.expiresAt)},
52
+ ${execution.cwd ?? null},
53
+ ${execution.blastRadius ? sql.json(execution.blastRadius) : null},
54
+ ${hash},
55
+ ${prevHash}
56
+ )
57
+ ON CONFLICT (token) DO NOTHING
58
+ `;
59
+ }
60
+ async get(token) {
61
+ const sql = getSql();
62
+ const rows = await sql`
63
+ SELECT * FROM pending_approvals
64
+ WHERE token = ${token}
65
+ AND resolved_at IS NULL
66
+ AND expires_at > NOW()
67
+ `;
68
+ if (rows.length === 0) return null;
69
+ return rowToPending(rows[0]);
70
+ }
71
+ async resolve(token) {
72
+ const sql = getSql();
73
+ const rows = await sql`
74
+ UPDATE pending_approvals
75
+ SET resolved_at = NOW(), resolution = 'resolved'
76
+ WHERE token = ${token} AND resolved_at IS NULL
77
+ RETURNING 1
78
+ `;
79
+ return rows.length > 0;
80
+ }
81
+ async listPending(tenantId = DEFAULT_TENANT) {
82
+ const sql = getSql();
83
+ const rows = await sql`
84
+ SELECT * FROM pending_approvals
85
+ WHERE tenant_id = ${tenantId} AND resolved_at IS NULL AND expires_at > NOW()
86
+ ORDER BY requested_at ASC
87
+ `;
88
+ return rows.map((r) => [
89
+ String(r.token ?? ""),
90
+ rowToPending(r)
91
+ ]);
92
+ }
93
+ async pruneExpired() {
94
+ const sql = getSql();
95
+ await sql`
96
+ UPDATE pending_approvals
97
+ SET resolved_at = NOW(), resolution = 'expired'
98
+ WHERE expires_at < NOW() AND resolved_at IS NULL
99
+ `;
100
+ }
101
+ async flush() {
102
+ return Promise.resolve();
103
+ }
104
+ }
105
+ export {
106
+ PgApprovalStore
107
+ };
@@ -0,0 +1,193 @@
1
+ import { createHash, randomUUID } from "crypto";
2
+ import { getSql } from "./pg-client.js";
3
+ import { MAX_BLUNDERS, NON_BLOCKING_BLUNDER_TYPES } from "../../sensor/agent-blunder-store.js";
4
+ const DEFAULT_TENANT = "local";
5
+ const GENESIS_HASH = "0".repeat(64);
6
+ function _hashableContent(event, previousHash) {
7
+ const content = {
8
+ id: event.id,
9
+ recordedAt: event.recordedAt,
10
+ blunderType: event.blunderType,
11
+ command: event.command,
12
+ blockReason: event.blockReason,
13
+ service: event.service,
14
+ tag: event.tag,
15
+ actor: event.actor,
16
+ pid: event.pid,
17
+ confidenceScore: event.confidenceScore
18
+ };
19
+ if (event.triggeredRules !== void 0) content.triggeredRules = event.triggeredRules;
20
+ return previousHash + JSON.stringify(content);
21
+ }
22
+ function _computeHash(event, previousHash) {
23
+ return createHash("sha256").update(_hashableContent(event, previousHash)).digest("hex");
24
+ }
25
+ function rowToBlunder(row) {
26
+ const triggeredRaw = row.triggered_rules;
27
+ let triggeredRules;
28
+ if (triggeredRaw === null || triggeredRaw === void 0) {
29
+ triggeredRules = null;
30
+ } else if (Array.isArray(triggeredRaw)) {
31
+ triggeredRules = triggeredRaw;
32
+ } else {
33
+ try {
34
+ triggeredRules = JSON.parse(String(triggeredRaw));
35
+ } catch {
36
+ triggeredRules = null;
37
+ }
38
+ }
39
+ return {
40
+ id: String(row.id ?? ""),
41
+ recordedAt: Number(row.recorded_at ?? 0),
42
+ blunderType: row.blunder_type ?? "allowlist_block",
43
+ command: row.command ? String(row.command) : null,
44
+ blockReason: String(row.block_reason ?? ""),
45
+ service: row.service ? String(row.service) : null,
46
+ tag: row.tag ? String(row.tag) : null,
47
+ actor: row.actor ? String(row.actor) : null,
48
+ pid: row.pid ? String(row.pid) : null,
49
+ confidenceScore: row.confidence_score != null ? Number(row.confidence_score) : null,
50
+ triggeredRules,
51
+ previousHash: String(row.previous_hash ?? ""),
52
+ hash: String(row.hash ?? "")
53
+ };
54
+ }
55
+ class PgBlunderStore {
56
+ async record(event, tenantId) {
57
+ const sql = getSql();
58
+ const tid = tenantId ?? DEFAULT_TENANT;
59
+ await sql.begin("SERIALIZABLE", async (tx) => {
60
+ const id = event.id ?? randomUUID();
61
+ const existing = await tx`SELECT 1 FROM agent_blunders WHERE id = ${id} AND tenant_id = ${tid} LIMIT 1`;
62
+ if (existing.length > 0) return;
63
+ const lastRows = await tx`
64
+ SELECT hash FROM agent_blunders
65
+ WHERE tenant_id = ${tid}
66
+ ORDER BY recorded_at ASC, id ASC
67
+ OFFSET (SELECT GREATEST(COUNT(*) - 1, 0) FROM agent_blunders WHERE tenant_id = ${tid})
68
+ LIMIT 1
69
+ `;
70
+ const previousHash = lastRows.length > 0 ? String(lastRows[0].hash) : GENESIS_HASH;
71
+ const recordedAt = event.recordedAt ?? Date.now();
72
+ const triggeredRules = event.triggeredRules !== void 0 ? event.triggeredRules : null;
73
+ const base = { ...event, id, recordedAt, triggeredRules };
74
+ const hash = _computeHash(base, previousHash);
75
+ await tx`
76
+ INSERT INTO agent_blunders (
77
+ id, tenant_id, recorded_at, blunder_type, command, block_reason,
78
+ service, tag, actor, pid, confidence_score, triggered_rules,
79
+ previous_hash, hash
80
+ ) VALUES (
81
+ ${id}, ${tid}, ${recordedAt},
82
+ ${event.blunderType}, ${event.command ?? null}, ${event.blockReason},
83
+ ${event.service ?? null}, ${event.tag ?? null}, ${event.actor ?? null},
84
+ ${event.pid ?? null}, ${event.confidenceScore ?? null},
85
+ ${triggeredRules != null ? JSON.stringify(triggeredRules) : null},
86
+ ${previousHash}, ${hash}
87
+ )
88
+ ON CONFLICT (id, tenant_id) DO NOTHING
89
+ `;
90
+ await tx`
91
+ DELETE FROM agent_blunders
92
+ WHERE tenant_id = ${tid}
93
+ AND recorded_at = (
94
+ SELECT recorded_at FROM agent_blunders
95
+ WHERE tenant_id = ${tid}
96
+ ORDER BY recorded_at ASC, id ASC
97
+ LIMIT 1
98
+ )
99
+ AND (SELECT COUNT(*) FROM agent_blunders WHERE tenant_id = ${tid}) > ${MAX_BLUNDERS}
100
+ `;
101
+ });
102
+ }
103
+ async list(tenantId) {
104
+ const sql = getSql();
105
+ const tid = tenantId ?? DEFAULT_TENANT;
106
+ const rows = await sql`
107
+ SELECT * FROM agent_blunders
108
+ WHERE tenant_id = ${tid}
109
+ ORDER BY recorded_at ASC, id ASC
110
+ `;
111
+ return rows.map((r) => rowToBlunder(r));
112
+ }
113
+ async getStats(tenantId) {
114
+ const sql = getSql();
115
+ const tid = tenantId ?? DEFAULT_TENANT;
116
+ const now = Date.now();
117
+ const ms7 = 7 * 24 * 60 * 60 * 1e3;
118
+ const ms30 = 30 * 24 * 60 * 60 * 1e3;
119
+ const nonBlockingTypes = [...NON_BLOCKING_BLUNDER_TYPES];
120
+ const [totalRows, typeRows, recent7Rows, recent30Rows] = await Promise.all([
121
+ sql`SELECT COUNT(*) AS cnt FROM agent_blunders WHERE tenant_id = ${tid} AND blunder_type != ALL(${nonBlockingTypes})`,
122
+ sql`SELECT blunder_type, COUNT(*) AS cnt FROM agent_blunders WHERE tenant_id = ${tid} GROUP BY blunder_type`,
123
+ sql`SELECT COUNT(*) AS cnt FROM agent_blunders WHERE tenant_id = ${tid} AND recorded_at >= ${now - ms7} AND blunder_type != ALL(${nonBlockingTypes})`,
124
+ sql`SELECT COUNT(*) AS cnt FROM agent_blunders WHERE tenant_id = ${tid} AND recorded_at >= ${now - ms30} AND blunder_type != ALL(${nonBlockingTypes})`
125
+ ]);
126
+ const byType = {};
127
+ for (const row of typeRows) {
128
+ byType[String(row.blunder_type)] = Number(row.cnt);
129
+ }
130
+ return {
131
+ total: Number(totalRows[0].cnt),
132
+ byType,
133
+ last7Days: Number(recent7Rows[0].cnt),
134
+ last30Days: Number(recent30Rows[0].cnt)
135
+ };
136
+ }
137
+ async verifyChain(tenantId) {
138
+ const hmacProtected = false;
139
+ const blunders = await this.list(tenantId);
140
+ if (blunders.length === 0) {
141
+ return { valid: true, verified: 0, note: "Log is empty \u2014 nothing to verify", tamperEvidenceLevel: "none", hmacProtected };
142
+ }
143
+ const firstV2Idx = blunders.findIndex((b) => !!b.hash);
144
+ if (firstV2Idx === -1) {
145
+ return {
146
+ valid: true,
147
+ verified: 0,
148
+ truncated: false,
149
+ note: "No cryptographically-verified entries \u2014 all entries predate hash-chain migration. Chain integrity cannot be confirmed.",
150
+ tamperEvidenceLevel: "none",
151
+ hmacProtected
152
+ };
153
+ }
154
+ const anchor = blunders[firstV2Idx];
155
+ const truncated = anchor.previousHash !== GENESIS_HASH;
156
+ let expectedPrev = anchor.previousHash;
157
+ let seenFirstV2 = false;
158
+ for (let i = firstV2Idx; i < blunders.length; i++) {
159
+ const b = blunders[i];
160
+ if (!b.hash) {
161
+ return { valid: false, firstInvalidIdx: i, reason: `entry at index ${i} (id=${b.id}) is missing hash field after v2 section began \u2014 possible injection`, tamperEvidenceLevel: "hash-chain", hmacProtected };
162
+ }
163
+ if (!seenFirstV2) {
164
+ seenFirstV2 = true;
165
+ } else {
166
+ if (b.previousHash !== expectedPrev) {
167
+ return { valid: false, firstInvalidIdx: i, reason: `previousHash mismatch at index ${i}: expected ${expectedPrev.slice(0, 8)}\u2026 got ${b.previousHash.slice(0, 8)}\u2026`, tamperEvidenceLevel: "hash-chain", hmacProtected };
168
+ }
169
+ }
170
+ const { hash: _h, previousHash: _p, ...rest } = b;
171
+ const recomputed = _computeHash(rest, b.previousHash);
172
+ if (recomputed !== b.hash) {
173
+ return { valid: false, firstInvalidIdx: i, reason: `hash mismatch at index ${i} (id=${b.id}): content was modified after recording`, tamperEvidenceLevel: "hash-chain", hmacProtected };
174
+ }
175
+ expectedPrev = b.hash;
176
+ }
177
+ return {
178
+ valid: true,
179
+ truncated,
180
+ verifiedFrom: anchor.id,
181
+ verified: blunders.length - firstV2Idx,
182
+ tamperEvidenceLevel: "hash-chain",
183
+ hmacProtected
184
+ };
185
+ }
186
+ /** Always false in Postgres mode — DB integrity mechanisms replace the HMAC sidecar. */
187
+ isIntegrityViolated() {
188
+ return false;
189
+ }
190
+ }
191
+ export {
192
+ PgBlunderStore
193
+ };
@@ -0,0 +1,83 @@
1
+ import { randomUUID } from "crypto";
2
+ import { getSql } from "./pg-client.js";
3
+ import { MAX_GATE_HISTORY } from "../../sensor/ci-gate-history.js";
4
+ import logger from "../../sensor/logger.js";
5
+ const DEFAULT_TENANT = "local";
6
+ function rowToEntry(row) {
7
+ const reasonsRaw = row.reasons;
8
+ let reasons = [];
9
+ if (Array.isArray(reasonsRaw)) {
10
+ reasons = reasonsRaw;
11
+ } else if (typeof reasonsRaw === "string") {
12
+ try {
13
+ reasons = JSON.parse(reasonsRaw);
14
+ } catch {
15
+ reasons = [];
16
+ }
17
+ }
18
+ return {
19
+ id: String(row.id ?? ""),
20
+ recordedAt: Number(row.recorded_at ?? 0),
21
+ prNumber: row.pr_number != null ? Number(row.pr_number) : null,
22
+ repo: row.repo ? String(row.repo) : null,
23
+ service: row.service ? String(row.service) : null,
24
+ actor: row.actor ? String(row.actor) : null,
25
+ verdict: row.verdict ?? "pass",
26
+ riskScore: Number(row.risk_score ?? 0),
27
+ reasons,
28
+ sha: row.sha ? String(row.sha) : null
29
+ };
30
+ }
31
+ class PgGateHistoryStore {
32
+ async record(entry, tenantId) {
33
+ const tid = tenantId ?? DEFAULT_TENANT;
34
+ const id = entry.id ?? randomUUID();
35
+ const recordedAt = entry.recordedAt ?? Date.now();
36
+ try {
37
+ const sql = getSql();
38
+ await sql.begin(async (tx) => {
39
+ const existing = await tx`SELECT 1 FROM ci_gate_history WHERE id = ${id} AND tenant_id = ${tid} LIMIT 1`;
40
+ if (existing.length > 0) return;
41
+ await tx`
42
+ INSERT INTO ci_gate_history (
43
+ id, tenant_id, recorded_at, pr_number, repo, service, actor, verdict, risk_score, reasons, sha
44
+ ) VALUES (
45
+ ${id}, ${tid}, ${recordedAt},
46
+ ${entry.prNumber ?? null}, ${entry.repo ?? null}, ${entry.service ?? null}, ${entry.actor ?? null},
47
+ ${entry.verdict}, ${entry.riskScore ?? 0}, ${JSON.stringify(entry.reasons ?? [])}, ${entry.sha ?? null}
48
+ )
49
+ ON CONFLICT (id, tenant_id) DO NOTHING
50
+ `;
51
+ await tx`
52
+ DELETE FROM ci_gate_history
53
+ WHERE tenant_id = ${tid}
54
+ AND recorded_at = (
55
+ SELECT recorded_at FROM ci_gate_history
56
+ WHERE tenant_id = ${tid}
57
+ ORDER BY recorded_at ASC, id ASC
58
+ LIMIT 1
59
+ )
60
+ AND (SELECT COUNT(*) FROM ci_gate_history WHERE tenant_id = ${tid}) > ${MAX_GATE_HISTORY}
61
+ `;
62
+ });
63
+ } catch (err) {
64
+ logger.warn({ err }, "pg-ci-gate-history: record failed (non-fatal)");
65
+ }
66
+ }
67
+ async listForPR(prNumber, repo, tenantId) {
68
+ const sql = getSql();
69
+ const tid = tenantId ?? DEFAULT_TENANT;
70
+ const rows = repo ? await sql`SELECT * FROM ci_gate_history WHERE tenant_id = ${tid} AND pr_number = ${prNumber} AND repo = ${repo} ORDER BY recorded_at ASC` : await sql`SELECT * FROM ci_gate_history WHERE tenant_id = ${tid} AND pr_number = ${prNumber} ORDER BY recorded_at ASC`;
71
+ return rows.map((r) => rowToEntry(r));
72
+ }
73
+ async list(opts, tenantId) {
74
+ const sql = getSql();
75
+ const tid = tenantId ?? DEFAULT_TENANT;
76
+ const limit = opts?.limit ?? 100;
77
+ const rows = await sql`SELECT * FROM ci_gate_history WHERE tenant_id = ${tid} ORDER BY recorded_at DESC LIMIT ${limit}`;
78
+ return rows.map((r) => rowToEntry(r));
79
+ }
80
+ }
81
+ export {
82
+ PgGateHistoryStore
83
+ };
@@ -0,0 +1,24 @@
1
+ import postgres from "postgres";
2
+ let _sql = null;
3
+ function getSql() {
4
+ if (!_sql) {
5
+ const url = process.env.MERGEN_PG_URL;
6
+ if (!url) throw new Error("MERGEN_PG_URL is not set");
7
+ _sql = postgres(url, {
8
+ max: 10,
9
+ idle_timeout: 30,
10
+ connect_timeout: 10
11
+ });
12
+ }
13
+ return _sql;
14
+ }
15
+ async function closeSql() {
16
+ if (_sql) {
17
+ await _sql.end();
18
+ _sql = null;
19
+ }
20
+ }
21
+ export {
22
+ closeSql,
23
+ getSql
24
+ };
@@ -0,0 +1,63 @@
1
+ import { getSql } from "./pg-client.js";
2
+ const DEFAULT_TENANT = "local";
3
+ const DEFAULT_RETENTION_HOURS = 72;
4
+ class PgEventStore {
5
+ async init() {
6
+ }
7
+ async push(event, tenantId) {
8
+ const sql = getSql();
9
+ const tid = tenantId ?? DEFAULT_TENANT;
10
+ const type = event.type;
11
+ const ts = event.timestamp ?? Date.now();
12
+ let level = null;
13
+ if (event.type === "console") {
14
+ level = event.level ?? null;
15
+ } else if (event.type === "network") {
16
+ const status = event.status;
17
+ level = status != null ? String(status) : null;
18
+ }
19
+ await sql`
20
+ INSERT INTO events (tenant_id, type, level, data, ts)
21
+ VALUES (${tid}, ${type}, ${level}, ${sql.json(event)}, ${ts})
22
+ `;
23
+ }
24
+ async query(opts) {
25
+ const sql = getSql();
26
+ const tid = opts.tenantId ?? DEFAULT_TENANT;
27
+ const limit = opts.limit ?? 500;
28
+ const rows = await sql`
29
+ SELECT data FROM events
30
+ WHERE tenant_id = ${tid}
31
+ ${opts.since != null ? sql`AND ts >= ${opts.since}` : sql``}
32
+ ${opts.level != null ? sql`AND level = ${opts.level}` : sql``}
33
+ ${opts.type != null ? sql`AND type = ${opts.type}` : sql``}
34
+ ORDER BY ts DESC
35
+ LIMIT ${limit}
36
+ `;
37
+ return rows.map((r) => r.data);
38
+ }
39
+ async size() {
40
+ const sql = getSql();
41
+ const rows = await sql`SELECT COUNT(*)::int AS cnt FROM events`;
42
+ return rows[0]?.cnt ?? 0;
43
+ }
44
+ async clear(tenantId) {
45
+ const sql = getSql();
46
+ if (tenantId) {
47
+ await sql`DELETE FROM events WHERE tenant_id = ${tenantId}`;
48
+ } else {
49
+ await sql`DELETE FROM events`;
50
+ }
51
+ }
52
+ async pruneOld() {
53
+ const sql = getSql();
54
+ const hours = Number(process.env.MERGEN_RETENTION_HOURS ?? DEFAULT_RETENTION_HOURS);
55
+ await sql`
56
+ DELETE FROM events
57
+ WHERE inserted_at < NOW() - (${hours} || ' hours')::interval
58
+ `;
59
+ }
60
+ }
61
+ export {
62
+ PgEventStore
63
+ };
@@ -0,0 +1,190 @@
1
+ import { getSql } from "./pg-client.js";
2
+ const DEFAULT_TENANT = "local";
3
+ function toMs(value) {
4
+ if (!value) return 0;
5
+ if (value instanceof Date) return value.getTime();
6
+ return new Date(value).getTime();
7
+ }
8
+ function toMsNullable(value) {
9
+ if (!value) return null;
10
+ if (value instanceof Date) return value.getTime();
11
+ return new Date(value).getTime();
12
+ }
13
+ function rowToIncident(row) {
14
+ return {
15
+ pid: String(row.pid ?? ""),
16
+ hypothesis: String(row.hypothesis ?? ""),
17
+ tag: String(row.tag ?? ""),
18
+ status: row.status ?? "open",
19
+ assignee: row.assignee ? String(row.assignee) : null,
20
+ notes: Array.isArray(row.notes) ? row.notes : (() => {
21
+ try {
22
+ return JSON.parse(String(row.notes ?? "[]"));
23
+ } catch {
24
+ return [];
25
+ }
26
+ })(),
27
+ sha: row.sha ? String(row.sha) : null,
28
+ environment: row.environment ? String(row.environment) : null,
29
+ service: row.service ? String(row.service) : null,
30
+ cluster: row.cluster ? String(row.cluster) : null,
31
+ confidence: Number(row.confidence ?? 0),
32
+ createdAt: toMs(row.created_at),
33
+ updatedAt: toMs(row.updated_at),
34
+ acknowledgedBy: row.acknowledged_by ? String(row.acknowledged_by) : null,
35
+ resolvedAt: toMsNullable(row.resolved_at),
36
+ resolvedAutonomously: Boolean(row.resolved_autonomously),
37
+ causallyCorrect: Boolean(row.causally_correct),
38
+ contextBriefViewedAt: toMsNullable(row.context_brief_viewed_at)
39
+ };
40
+ }
41
+ class PgIncidentStore {
42
+ async init() {
43
+ }
44
+ async upsert(pid, fields, tenantId) {
45
+ const sql = getSql();
46
+ const tid = tenantId ?? DEFAULT_TENANT;
47
+ const notes = JSON.stringify(fields.notes ?? []);
48
+ const rows = await sql`
49
+ INSERT INTO incidents (
50
+ pid, tenant_id, hypothesis, tag, status, assignee, notes,
51
+ sha, environment, service, cluster, confidence,
52
+ acknowledged_by, resolved_at, resolved_autonomously, causally_correct
53
+ ) VALUES (
54
+ ${pid}, ${tid},
55
+ ${fields.hypothesis ?? ""},
56
+ ${fields.tag ?? ""},
57
+ ${fields.status ?? "open"},
58
+ ${fields.assignee ?? null},
59
+ ${notes}::jsonb,
60
+ ${fields.sha ?? null},
61
+ ${fields.environment ?? null},
62
+ ${fields.service ?? null},
63
+ ${fields.cluster ?? null},
64
+ ${fields.confidence ?? 0},
65
+ ${fields.acknowledgedBy ?? null},
66
+ ${fields.resolvedAt ? new Date(fields.resolvedAt) : null},
67
+ ${fields.resolvedAutonomously ?? false},
68
+ ${fields.causallyCorrect ?? false}
69
+ )
70
+ ON CONFLICT (tenant_id, pid) DO UPDATE SET
71
+ hypothesis = EXCLUDED.hypothesis,
72
+ tag = EXCLUDED.tag,
73
+ status = EXCLUDED.status,
74
+ assignee = EXCLUDED.assignee,
75
+ notes = EXCLUDED.notes,
76
+ sha = EXCLUDED.sha,
77
+ environment = EXCLUDED.environment,
78
+ service = EXCLUDED.service,
79
+ cluster = EXCLUDED.cluster,
80
+ confidence = EXCLUDED.confidence,
81
+ updated_at = NOW(),
82
+ acknowledged_by = EXCLUDED.acknowledged_by,
83
+ resolved_at = EXCLUDED.resolved_at,
84
+ resolved_autonomously = EXCLUDED.resolved_autonomously,
85
+ causally_correct = EXCLUDED.causally_correct
86
+ RETURNING *
87
+ `;
88
+ const incident = rowToIncident(rows[0]);
89
+ if (incident.service) {
90
+ await this.updateServiceEdges(incident.service, incident.updatedAt, void 0, tid);
91
+ }
92
+ return incident;
93
+ }
94
+ async get(pid, tenantId) {
95
+ const sql = getSql();
96
+ const tid = tenantId ?? DEFAULT_TENANT;
97
+ const rows = await sql`
98
+ SELECT * FROM incidents WHERE tenant_id = ${tid} AND pid = ${pid}
99
+ `;
100
+ if (rows.length === 0) return null;
101
+ return rowToIncident(rows[0]);
102
+ }
103
+ async list(status, limit = 50, tenantId) {
104
+ const sql = getSql();
105
+ const tid = tenantId ?? DEFAULT_TENANT;
106
+ const rows = await sql`
107
+ SELECT * FROM incidents
108
+ WHERE tenant_id = ${tid}
109
+ ${status != null ? sql`AND status = ${status}` : sql``}
110
+ ORDER BY updated_at DESC
111
+ LIMIT ${limit}
112
+ `;
113
+ return rows.map((r) => rowToIncident(r));
114
+ }
115
+ async addNote(pid, note, author, tenantId) {
116
+ const inc = await this.get(pid, tenantId);
117
+ if (!inc) return null;
118
+ const entry = author ? `[${author}] ${note}` : note;
119
+ return this.upsert(pid, { notes: [...inc.notes, entry] }, tenantId);
120
+ }
121
+ async markContextViewed(pid, tenantId) {
122
+ const sql = getSql();
123
+ const tid = tenantId ?? DEFAULT_TENANT;
124
+ await sql`
125
+ UPDATE incidents
126
+ SET context_brief_viewed_at = NOW()
127
+ WHERE tenant_id = ${tid} AND pid = ${pid} AND context_brief_viewed_at IS NULL
128
+ `;
129
+ }
130
+ async coOccurringServices(service, windowMs = 10 * 60 * 1e3, limit = 4, tenantId) {
131
+ const sql = getSql();
132
+ const tid = tenantId ?? DEFAULT_TENANT;
133
+ const intervalSec = Math.round(windowMs / 1e3);
134
+ const rows = await sql`
135
+ SELECT other.service, COUNT(*) AS cnt
136
+ FROM incidents AS base
137
+ JOIN incidents AS other
138
+ ON other.service != base.service
139
+ AND other.service IS NOT NULL
140
+ AND other.created_at BETWEEN base.created_at - (${intervalSec} || ' seconds')::interval
141
+ AND base.created_at + (${intervalSec} || ' seconds')::interval
142
+ WHERE base.tenant_id = ${tid}
143
+ AND other.tenant_id = ${tid}
144
+ AND base.service = ${service}
145
+ GROUP BY other.service
146
+ ORDER BY cnt DESC
147
+ LIMIT ${limit}
148
+ `;
149
+ return rows.map((r) => ({ service: String(r.service ?? ""), count: Number(r.cnt ?? 0) }));
150
+ }
151
+ async updateServiceEdges(service, at, windowMs = 10 * 60 * 1e3, tenantId) {
152
+ const sql = getSql();
153
+ const tid = tenantId ?? DEFAULT_TENANT;
154
+ const coServices = await this.coOccurringServices(service, windowMs, 20, tid);
155
+ const lastAt = new Date(at);
156
+ for (const { service: target } of coServices) {
157
+ for (const [src, tgt] of [[service, target], [target, service]]) {
158
+ await sql`
159
+ INSERT INTO service_edges (tenant_id, source, target, weight, last_incident_at)
160
+ VALUES (${tid}, ${src}, ${tgt}, 1, ${lastAt})
161
+ ON CONFLICT (tenant_id, source, target)
162
+ DO UPDATE SET
163
+ weight = service_edges.weight + 1,
164
+ last_incident_at = EXCLUDED.last_incident_at
165
+ `;
166
+ }
167
+ }
168
+ }
169
+ async getInteractionGraph(service, tenantId) {
170
+ const sql = getSql();
171
+ const tid = tenantId ?? DEFAULT_TENANT;
172
+ const rows = await sql`
173
+ SELECT source, target, weight, last_incident_at
174
+ FROM service_edges
175
+ WHERE tenant_id = ${tid}
176
+ ${service != null ? sql`AND (source = ${service} OR target = ${service})` : sql``}
177
+ ORDER BY weight DESC
178
+ LIMIT ${service != null ? 100 : 200}
179
+ `;
180
+ return rows.map((r) => ({
181
+ source: String(r.source ?? ""),
182
+ target: String(r.target ?? ""),
183
+ weight: Number(r.weight ?? 0),
184
+ lastIncidentAt: toMs(r.last_incident_at)
185
+ }));
186
+ }
187
+ }
188
+ export {
189
+ PgIncidentStore
190
+ };
@@ -0,0 +1,26 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { getSql } from "./pg-client.js";
5
+ async function runMigrations() {
6
+ const sql = getSql();
7
+ await sql`
8
+ CREATE TABLE IF NOT EXISTS _migrations (
9
+ id SERIAL PRIMARY KEY,
10
+ name TEXT NOT NULL UNIQUE,
11
+ applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
12
+ )
13
+ `;
14
+ const migrationsDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "migrations");
15
+ const files = fs.readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort();
16
+ for (const file of files) {
17
+ const applied = await sql`SELECT 1 FROM _migrations WHERE name = ${file}`;
18
+ if (applied.length > 0) continue;
19
+ const content = fs.readFileSync(path.join(migrationsDir, file), "utf8");
20
+ await sql.unsafe(content);
21
+ await sql`INSERT INTO _migrations (name) VALUES (${file})`;
22
+ }
23
+ }
24
+ export {
25
+ runMigrations
26
+ };