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,103 @@
1
+ import crypto from "crypto";
2
+ import { loadEnterprisePolicy, saveEnterprisePolicy, IMMUTABLE_RULE_IDS } from "./enterprise-policy-engine.js";
3
+ import logger from "../sensor/logger.js";
4
+ let _lastEtag = "";
5
+ function verifyPolicySignature(body, signature, secret) {
6
+ if (!signature) return false;
7
+ const [algo, hex] = signature.split("=");
8
+ if (algo !== "sha256" || !hex) return false;
9
+ const expected = crypto.createHmac("sha256", secret).update(body).digest("hex");
10
+ return crypto.timingSafeEqual(Buffer.from(hex, "hex"), Buffer.from(expected, "hex"));
11
+ }
12
+ async function fetchAndApply(opts) {
13
+ const hmacSecret = process.env.MERGEN_POLICY_HMAC_SECRET;
14
+ const headers = { "Accept": "application/json" };
15
+ if (_lastEtag) headers["If-None-Match"] = _lastEtag;
16
+ let resp;
17
+ try {
18
+ resp = await fetch(opts.url, { headers, signal: AbortSignal.timeout(1e4) });
19
+ } catch (err) {
20
+ logger.warn({ err, url: opts.url }, "policy-sync: fetch failed (non-fatal)");
21
+ return;
22
+ }
23
+ if (resp.status === 304) {
24
+ logger.debug({ url: opts.url }, "policy-sync: policy unchanged");
25
+ return;
26
+ }
27
+ if (!resp.ok) {
28
+ logger.warn({ status: resp.status, url: opts.url }, "policy-sync: remote returned non-2xx (non-fatal)");
29
+ return;
30
+ }
31
+ const etag = resp.headers.get("etag") ?? "";
32
+ let rawBody;
33
+ try {
34
+ rawBody = await resp.text();
35
+ } catch (err) {
36
+ logger.warn({ err }, "policy-sync: could not read response body");
37
+ return;
38
+ }
39
+ if (hmacSecret) {
40
+ const sig = resp.headers.get("x-mergen-policy-signature");
41
+ if (!verifyPolicySignature(rawBody, sig, hmacSecret)) {
42
+ logger.error(
43
+ { url: opts.url, hasSignature: !!sig },
44
+ "policy-sync: HMAC signature verification failed \u2014 remote policy rejected. Ensure the policy server sends X-Mergen-Policy-Signature: sha256=<hmac> keyed with MERGEN_POLICY_HMAC_SECRET."
45
+ );
46
+ return;
47
+ }
48
+ logger.debug({ url: opts.url }, "policy-sync: HMAC signature verified");
49
+ }
50
+ let incoming;
51
+ try {
52
+ incoming = JSON.parse(rawBody);
53
+ } catch (err) {
54
+ logger.warn({ err }, "policy-sync: could not parse remote policy JSON");
55
+ return;
56
+ }
57
+ const local = loadEnterprisePolicy();
58
+ const mode = opts.mergeMode ?? "replace";
59
+ let merged;
60
+ if (mode === "merge") {
61
+ const existingIds = new Set(local.rules.map((r) => r.id));
62
+ merged = { ...local, rules: [...local.rules, ...incoming.rules.filter((r) => !existingIds.has(r.id))] };
63
+ } else {
64
+ const immutableRules = local.rules.filter((r) => IMMUTABLE_RULE_IDS.has(r.id));
65
+ const remoteRulesWithoutImmutable = incoming.rules.filter((r) => !IMMUTABLE_RULE_IDS.has(r.id));
66
+ merged = { ...incoming, rules: [...immutableRules, ...remoteRulesWithoutImmutable] };
67
+ const keptIds = immutableRules.map((r) => r.id);
68
+ if (keptIds.length > 0) {
69
+ logger.info({ keptIds }, "policy-sync: preserved immutable local rules during replace");
70
+ }
71
+ }
72
+ try {
73
+ saveEnterprisePolicy(merged);
74
+ _lastEtag = etag;
75
+ logger.info({ url: opts.url, mode, ruleCount: merged.rules.length }, "policy-sync: policy synced");
76
+ } catch (err) {
77
+ logger.warn({ err }, "policy-sync: failed to save synced policy (non-fatal)");
78
+ }
79
+ }
80
+ function startPolicySync(opts) {
81
+ const intervalMs = opts.intervalMs ?? 5 * 60 * 1e3;
82
+ if (!opts.url.startsWith("https://")) {
83
+ logger.error(
84
+ { url: opts.url },
85
+ "policy-sync: MERGEN_POLICY_URL must use HTTPS to prevent policy injection over plain HTTP. Update MERGEN_POLICY_URL to an https:// address and restart."
86
+ );
87
+ return;
88
+ }
89
+ const hmacSecret = process.env.MERGEN_POLICY_HMAC_SECRET;
90
+ if (!hmacSecret) {
91
+ logger.warn(
92
+ { url: opts.url },
93
+ "policy-sync: MERGEN_POLICY_HMAC_SECRET is not set \u2014 remote policy responses are not signature-verified. A compromised MERGEN_POLICY_URL could inject permissive rules. Set MERGEN_POLICY_HMAC_SECRET to a shared secret and configure the policy server to sign responses."
94
+ );
95
+ }
96
+ void fetchAndApply(opts);
97
+ const timer = setInterval(() => void fetchAndApply(opts), intervalMs);
98
+ timer.unref();
99
+ logger.info({ url: opts.url, intervalMs, mergeMode: opts.mergeMode ?? "replace", hmacVerification: !!hmacSecret }, "policy-sync: started");
100
+ }
101
+ export {
102
+ startPolicySync
103
+ };
@@ -0,0 +1,35 @@
1
+ import { evaluateEnterprisePolicy } from "./enterprise-policy-engine.js";
2
+ function runPolicyTestCases(cases, policyOverride) {
3
+ const results = cases.map((tc) => {
4
+ const result = evaluateEnterprisePolicy(tc.input, policyOverride);
5
+ return {
6
+ id: tc.id,
7
+ description: tc.description,
8
+ expected: tc.expectedVerdict,
9
+ got: result.verdict,
10
+ passed: result.verdict === tc.expectedVerdict,
11
+ reasons: result.reasons
12
+ };
13
+ });
14
+ const passed = results.filter((r) => r.passed).length;
15
+ return { results, passed, failed: results.length - passed, total: results.length, allPassed: passed === results.length };
16
+ }
17
+ function parsePolicyTestFile(raw) {
18
+ const cases = Array.isArray(raw) ? raw : raw?.cases;
19
+ if (!Array.isArray(cases)) {
20
+ throw new Error('Expected a JSON array of test cases, or an object with a top-level "cases" array.');
21
+ }
22
+ for (const [i, tc] of cases.entries()) {
23
+ const c = tc;
24
+ if (typeof c?.id !== "string" || !c.id) throw new Error(`Test case at index ${i} is missing a string "id".`);
25
+ if (!c.input || typeof c.input !== "object") throw new Error(`Test case "${c.id}" is missing an "input" object.`);
26
+ if (!["block", "warn", "pass"].includes(c.expectedVerdict)) {
27
+ throw new Error(`Test case "${c.id}" has an invalid "expectedVerdict" (must be block/warn/pass).`);
28
+ }
29
+ }
30
+ return cases;
31
+ }
32
+ export {
33
+ parsePolicyTestFile,
34
+ runPolicyTestCases
35
+ };
@@ -0,0 +1,181 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import fg from "fast-glob";
4
+ import { createHash } from "crypto";
5
+ import { adrStore } from "../sensor/adr-store.js";
6
+ import { postmortemStore } from "./postmortem-store.js";
7
+ import logger from "../sensor/logger.js";
8
+ function parseMttrToMs(text) {
9
+ if (!text || text === "unknown") return null;
10
+ const minMatch = text.match(/(\d+)m/i);
11
+ const secMatch = text.match(/(\d+)s/i);
12
+ let ms = 0;
13
+ if (minMatch) ms += parseInt(minMatch[1], 10) * 6e4;
14
+ if (secMatch) ms += parseInt(secMatch[1], 10) * 1e3;
15
+ if (ms > 0) return ms;
16
+ const num = parseInt(text, 10);
17
+ if (!isNaN(num)) return num * 1e3;
18
+ return null;
19
+ }
20
+ function deterministicUUID(input) {
21
+ const hash = createHash("md5").update(input).digest("hex");
22
+ return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-${hash.slice(12, 16)}-${hash.slice(16, 20)}-${hash.slice(20, 32)}`;
23
+ }
24
+ function parseAdrMarkdown(content) {
25
+ const titleMatch = content.match(/^#\s*(ADR-\d+):\s*(.*)/mi);
26
+ if (!titleMatch) return null;
27
+ const id = titleMatch[1].toUpperCase();
28
+ const title = titleMatch[2].trim();
29
+ const dateMatch = content.match(/\*\*Date:\*\*\s*(.*)/i);
30
+ const date = dateMatch ? dateMatch[1].trim() : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
31
+ const statusMatch = content.match(/\*\*Status:\*\*\s*(.*)/i);
32
+ let status = "proposed";
33
+ if (statusMatch) {
34
+ const s = statusMatch[1].trim().toLowerCase();
35
+ if (s === "accepted" || s === "deprecated" || s === "superseded" || s === "proposed") {
36
+ status = s;
37
+ }
38
+ }
39
+ const extractSection = (headerName) => {
40
+ const regex = new RegExp(`##\\s*${headerName}[\\s\\S]*?(?=##|$)`, "i");
41
+ const match = content.match(regex);
42
+ if (!match) return "";
43
+ return match[0].replace(new RegExp(`^##\\s*${headerName}`, "i"), "").trim();
44
+ };
45
+ const decision = extractSection("Decision");
46
+ const rationale = extractSection("Rationale");
47
+ const consequences = extractSection("Consequences");
48
+ const alternativesRaw = extractSection("Alternatives(?: considered)?");
49
+ const alternatives = [];
50
+ if (alternativesRaw) {
51
+ const items = alternativesRaw.match(/^\s*-\s*(.*)/gm);
52
+ if (items) {
53
+ for (const item of items) {
54
+ alternatives.push(item.replace(/^\s*-\s*/, "").trim());
55
+ }
56
+ } else {
57
+ alternatives.push(alternativesRaw);
58
+ }
59
+ }
60
+ return {
61
+ id,
62
+ title,
63
+ status,
64
+ date,
65
+ decision,
66
+ alternatives,
67
+ rationale,
68
+ consequences
69
+ };
70
+ }
71
+ function parsePostmortemMarkdown(filename, content) {
72
+ const tagMatch = content.match(/^#\s*Postmortem\s*—\s*(.*)/mi);
73
+ if (!tagMatch) return null;
74
+ let tag = tagMatch[1].trim();
75
+ if (!tag.startsWith("infra_")) {
76
+ tag = `infra_${tag}`;
77
+ }
78
+ const serviceMatch = content.match(/\*\*Service:\*\*\s*([^\s|]+)/i);
79
+ const service = serviceMatch ? serviceMatch[1].trim() : "unknown";
80
+ const dateMatch = content.match(/\*\*Date:\*\*\s*([^\s|]+)/i);
81
+ const dateStr = dateMatch ? dateMatch[1].trim() : "";
82
+ const generatedAt = dateStr ? new Date(dateStr).getTime() : Date.now();
83
+ const confidenceMatch = content.match(/\*\*Confidence:\*\*\s*(\d+)%/i);
84
+ const confidence = confidenceMatch ? parseInt(confidenceMatch[1], 10) / 100 : 0.85;
85
+ const mttrMatch = content.match(/\*\*MTTR:\*\*\s*([^|]+)/i);
86
+ const mttrMs = mttrMatch ? parseMttrToMs(mttrMatch[1].trim()) : null;
87
+ const resolutionMatch = content.match(/\*\*Resolution:\*\*\s*(.*)/i);
88
+ const resolvedAutonomously = resolutionMatch ? /autonomous|mergen/i.test(resolutionMatch[1]) : false;
89
+ const branchMatch = content.match(/\*\*Branch:\*\*\s*([^\s|]+)/i);
90
+ const gitBranch = branchMatch ? branchMatch[1].trim() : null;
91
+ const shaMatch = content.match(/\*\*SHA:\*\*\s*([^\s|]+)/i);
92
+ const gitSha = shaMatch ? shaMatch[1].trim() : null;
93
+ const extractSection = (headerName) => {
94
+ const regex = new RegExp(`##\\s*${headerName}[\\s\\S]*?(?=##|$)`, "i");
95
+ const match = content.match(regex);
96
+ if (!match) return "";
97
+ return match[0].replace(new RegExp(`^##\\s*${headerName}`, "i"), "").trim();
98
+ };
99
+ const rootCause = extractSection("Root Cause") || "Unknown root cause";
100
+ const fixAppliedRaw = extractSection("Fix Applied");
101
+ let fixCommand = null;
102
+ if (fixAppliedRaw) {
103
+ const codeFenceMatch = fixAppliedRaw.match(/```(?:[a-zA-Z]*)\n([\s\S]*?)\n```/);
104
+ fixCommand = codeFenceMatch ? codeFenceMatch[1].trim() : fixAppliedRaw.trim();
105
+ }
106
+ const pid = deterministicUUID(path.basename(filename));
107
+ return {
108
+ pid,
109
+ tag,
110
+ service,
111
+ gitSha,
112
+ gitBranch,
113
+ rootCause,
114
+ fixCommand,
115
+ confidence,
116
+ mttrMs,
117
+ resolvedAutonomously,
118
+ causallyCorrect: true,
119
+ // imported postmortem was verified correct
120
+ generatedAt,
121
+ body: content
122
+ };
123
+ }
124
+ function findRepoRoot(startPath = process.cwd()) {
125
+ let curr = startPath;
126
+ while (curr && curr !== path.parse(curr).root) {
127
+ if (fs.existsSync(path.join(curr, ".git")) || fs.existsSync(path.join(curr, "docs/adr"))) {
128
+ return curr;
129
+ }
130
+ curr = path.dirname(curr);
131
+ }
132
+ return startPath;
133
+ }
134
+ async function syncMarkdownFilesFromDisk(startPath = process.cwd()) {
135
+ let adrsSynced = 0;
136
+ let postmortemsSynced = 0;
137
+ const repoRoot = findRepoRoot(startPath);
138
+ try {
139
+ const adrPattern = path.join(repoRoot, "docs/adr/ADR-*.md").replace(/\\/g, "/");
140
+ const adrFiles = await fg(adrPattern);
141
+ for (const file of adrFiles) {
142
+ try {
143
+ const content = fs.readFileSync(file, "utf8");
144
+ const adr = parseAdrMarkdown(content);
145
+ if (adr) {
146
+ adrStore.upsert(adr);
147
+ adrsSynced++;
148
+ }
149
+ } catch (err) {
150
+ logger.warn({ file, err }, "postmortem-parser: failed to sync ADR file");
151
+ }
152
+ }
153
+ const postmortemDir = path.join(repoRoot, "docs/postmortems");
154
+ if (fs.existsSync(postmortemDir)) {
155
+ const pmPattern = path.join(postmortemDir, "*.md").replace(/\\/g, "/");
156
+ const pmFiles = await fg(pmPattern);
157
+ for (const file of pmFiles) {
158
+ try {
159
+ const content = fs.readFileSync(file, "utf8");
160
+ const pm = parsePostmortemMarkdown(file, content);
161
+ if (pm) {
162
+ postmortemStore.write(pm);
163
+ postmortemsSynced++;
164
+ }
165
+ } catch (err) {
166
+ logger.warn({ file, err }, "postmortem-parser: failed to sync postmortem file");
167
+ }
168
+ }
169
+ }
170
+ logger.info({ adrsSynced, postmortemsSynced, repoRoot }, "postmortem-parser: sync completed");
171
+ } catch (err) {
172
+ logger.warn({ err }, "postmortem-parser: markdown files sync failed");
173
+ }
174
+ return { adrsSynced, postmortemsSynced };
175
+ }
176
+ export {
177
+ findRepoRoot,
178
+ parseAdrMarkdown,
179
+ parsePostmortemMarkdown,
180
+ syncMarkdownFilesFromDisk
181
+ };
@@ -0,0 +1,224 @@
1
+ import { postmortemStore } from "./postmortem-store.js";
2
+ const RRF_K = 60;
3
+ const STOPWORDS = /* @__PURE__ */ new Set([
4
+ "the",
5
+ "a",
6
+ "an",
7
+ "and",
8
+ "or",
9
+ "but",
10
+ "in",
11
+ "on",
12
+ "at",
13
+ "to",
14
+ "for",
15
+ "of",
16
+ "with",
17
+ "by",
18
+ "from",
19
+ "up",
20
+ "about",
21
+ "into",
22
+ "through",
23
+ "during",
24
+ "is",
25
+ "are",
26
+ "was",
27
+ "were",
28
+ "be",
29
+ "been",
30
+ "being",
31
+ "have",
32
+ "has",
33
+ "had",
34
+ "do",
35
+ "does",
36
+ "did",
37
+ "will",
38
+ "would",
39
+ "could",
40
+ "should",
41
+ "may",
42
+ "might",
43
+ "it",
44
+ "its",
45
+ "this",
46
+ "that",
47
+ "these",
48
+ "those",
49
+ "which",
50
+ "who",
51
+ "what",
52
+ "when",
53
+ "where",
54
+ "how",
55
+ "why",
56
+ "all",
57
+ "each",
58
+ "every",
59
+ "both",
60
+ "few",
61
+ "more",
62
+ "most",
63
+ "other",
64
+ "some",
65
+ "such",
66
+ "no",
67
+ "not",
68
+ "only",
69
+ "same",
70
+ "than",
71
+ "too",
72
+ "very",
73
+ "just",
74
+ "can",
75
+ "then",
76
+ "now",
77
+ "also",
78
+ "after",
79
+ "before",
80
+ "if",
81
+ "while",
82
+ "because",
83
+ "as",
84
+ "until",
85
+ "since",
86
+ "so"
87
+ ]);
88
+ function tokenize(text) {
89
+ return text.toLowerCase().replace(/[^\w\s_-]/g, " ").replace(/[_-]+/g, " ").split(/\s+/).filter((t) => t.length >= 2 && !STOPWORDS.has(t));
90
+ }
91
+ function buildIdf(documents) {
92
+ const N = documents.length;
93
+ const df = /* @__PURE__ */ new Map();
94
+ for (const tokens of documents) {
95
+ const seen = new Set(tokens);
96
+ for (const t of seen) {
97
+ df.set(t, (df.get(t) ?? 0) + 1);
98
+ }
99
+ }
100
+ const idf = /* @__PURE__ */ new Map();
101
+ for (const [term, count] of df) {
102
+ idf.set(term, Math.log(1 + N / (1 + count)));
103
+ }
104
+ return idf;
105
+ }
106
+ function computeTfIdf(tokens, idf) {
107
+ const tf = /* @__PURE__ */ new Map();
108
+ for (const t of tokens) {
109
+ tf.set(t, (tf.get(t) ?? 0) + 1);
110
+ }
111
+ const vec = /* @__PURE__ */ new Map();
112
+ const total = tokens.length || 1;
113
+ for (const [t, count] of tf) {
114
+ const idfScore = idf.get(t) ?? Math.log(2);
115
+ vec.set(t, count / total * idfScore);
116
+ }
117
+ return vec;
118
+ }
119
+ function cosineSimilarity(a, b) {
120
+ let dot = 0;
121
+ let normA = 0;
122
+ let normB = 0;
123
+ for (const [t, wa] of a) {
124
+ const wb = b.get(t) ?? 0;
125
+ dot += wa * wb;
126
+ normA += wa * wa;
127
+ }
128
+ for (const [, wb] of b) {
129
+ normB += wb * wb;
130
+ }
131
+ const denom = Math.sqrt(normA) * Math.sqrt(normB);
132
+ return denom === 0 ? 0 : dot / denom;
133
+ }
134
+ function embeddingSearch(query, postmortems, topK) {
135
+ if (postmortems.length === 0) return [];
136
+ const docTexts = postmortems.map(
137
+ (pm) => [pm.tag, pm.service, pm.rootCause, pm.fixCommand ?? "", pm.body].join(" ")
138
+ );
139
+ const docTokens = docTexts.map(tokenize);
140
+ const queryTokens = tokenize(query);
141
+ if (queryTokens.length === 0) return [];
142
+ const idf = buildIdf([...docTokens, queryTokens]);
143
+ const queryVec = computeTfIdf(queryTokens, idf);
144
+ const scored = postmortems.map((pm, i) => ({
145
+ pid: pm.pid,
146
+ similarity: cosineSimilarity(queryVec, computeTfIdf(docTokens[i], idf))
147
+ }));
148
+ return scored.filter((s) => s.similarity > 0).sort((a, b) => b.similarity - a.similarity).slice(0, topK).map((s, rank) => ({ ...s, rank }));
149
+ }
150
+ function reciprocalRankFusion(lists, k = RRF_K) {
151
+ const scores = /* @__PURE__ */ new Map();
152
+ for (const list of lists) {
153
+ for (const { pid, rank } of list) {
154
+ scores.set(pid, (scores.get(pid) ?? 0) + 1 / (k + rank + 1));
155
+ }
156
+ }
157
+ return scores;
158
+ }
159
+ function hybridSearch(query, opts = {}) {
160
+ const { tag, service, topK = 10, maxCorpus = 500 } = opts;
161
+ if (!query.trim()) return [];
162
+ let corpus;
163
+ if (tag) {
164
+ corpus = postmortemStore.getByTag(tag, maxCorpus);
165
+ } else {
166
+ corpus = postmortemStore.list(maxCorpus);
167
+ }
168
+ if (service) {
169
+ corpus = corpus.filter((pm) => pm.service === service);
170
+ }
171
+ if (corpus.length === 0) return [];
172
+ const keywordRanking = postmortemStore.keywordSearch(query, topK * 3);
173
+ const corpusPidSet = new Set(corpus.map((pm) => pm.pid));
174
+ const filteredKeyword = keywordRanking.filter((r) => corpusPidSet.has(r.pid));
175
+ const embeddingRanking = embeddingSearch(query, corpus, topK * 3);
176
+ const rrfScores = reciprocalRankFusion([filteredKeyword, embeddingRanking]);
177
+ const keywordRankMap = new Map(filteredKeyword.map((r) => [r.pid, r.rank]));
178
+ const embeddingMap = new Map(embeddingRanking.map((r) => [r.pid, r]));
179
+ const allPids = /* @__PURE__ */ new Set([
180
+ ...filteredKeyword.map((r) => r.pid),
181
+ ...embeddingRanking.map((r) => r.pid)
182
+ ]);
183
+ const pmMap = new Map(corpus.map((pm) => [pm.pid, pm]));
184
+ const results = [];
185
+ for (const pid of allPids) {
186
+ const pm = pmMap.get(pid);
187
+ if (!pm) continue;
188
+ const score = rrfScores.get(pid) ?? 0;
189
+ const inKeyword = keywordRankMap.has(pid);
190
+ const inEmbedding = embeddingMap.has(pid);
191
+ results.push({
192
+ postmortem: pm,
193
+ score,
194
+ signals: [
195
+ ...inKeyword ? ["keyword"] : [],
196
+ ...inEmbedding ? ["embedding"] : []
197
+ ],
198
+ embeddingSimilarity: embeddingMap.get(pid)?.similarity ?? null,
199
+ keywordRank: keywordRankMap.get(pid) ?? null
200
+ });
201
+ }
202
+ return results.sort((a, b) => b.score - a.score).slice(0, topK);
203
+ }
204
+ function tagToQuery(tag) {
205
+ const expansions = {
206
+ db_connection_pool_exhausted: "database connection pool exhausted timeout max connections",
207
+ oom_kill: "out of memory OOM kill process killed memory pressure container",
208
+ memory_pressure: "memory pressure high usage RSS heap limit",
209
+ rate_limit_cascade: "rate limit 429 throttle cascade downstream retry storm",
210
+ slow_query: "slow query timeout database latency lock wait",
211
+ downstream_latency_spike: "downstream dependency latency spike timeout 503",
212
+ certificate_expiry: "certificate expiry TLS SSL expired renewal",
213
+ disk_pressure: "disk full pressure inode storage volume mount",
214
+ queue_backlog: "queue backlog lag consumer worker stuck processing",
215
+ service_unavailable: "service unavailable 503 health check crash restart"
216
+ };
217
+ const clean = tag.replace(/^infra_/, "");
218
+ return expansions[clean] ?? clean.replace(/_/g, " ");
219
+ }
220
+ export {
221
+ hybridSearch,
222
+ tagToQuery,
223
+ tokenize
224
+ };