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,232 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { getStores } from "../storage/store-registry.js";
|
|
3
|
+
import { analyzeSemanticRisk } from "../intelligence/action-risk.js";
|
|
4
|
+
import { postmortemStore } from "../intelligence/postmortem-store.js";
|
|
5
|
+
import { evaluateEnterprisePolicy, isAiActor } from "../intelligence/enterprise-policy-engine.js";
|
|
6
|
+
import { evaluateDiffSize } from "../intelligence/diff-size.js";
|
|
7
|
+
import { serviceGraph } from "../sensor/service-graph.js";
|
|
8
|
+
import logger from "../sensor/logger.js";
|
|
9
|
+
function createCIGateRouter() {
|
|
10
|
+
const router = Router();
|
|
11
|
+
router.post("/ci/gate", async (req, res) => {
|
|
12
|
+
const body = req.body;
|
|
13
|
+
const files = Array.isArray(body.files) ? body.files : [];
|
|
14
|
+
const prTitle = typeof body.prTitle === "string" ? body.prTitle.slice(0, 300) : "";
|
|
15
|
+
const diff = typeof body.diff === "string" ? body.diff.slice(0, 2e4) : "";
|
|
16
|
+
const actor = typeof body.actor === "string" ? body.actor.slice(0, 100) : "unknown";
|
|
17
|
+
const diffStats = body.diffStats && typeof body.diffStats === "object" ? {
|
|
18
|
+
filesChanged: Number(body.diffStats.filesChanged) || 0,
|
|
19
|
+
additions: Number(body.diffStats.additions) || 0,
|
|
20
|
+
deletions: Number(body.diffStats.deletions) || 0
|
|
21
|
+
} : null;
|
|
22
|
+
const prNumber = typeof body.prNumber === "number" && Number.isFinite(body.prNumber) ? body.prNumber : null;
|
|
23
|
+
const repo = typeof body.repo === "string" ? body.repo.slice(0, 200) : null;
|
|
24
|
+
const sha = typeof body.sha === "string" ? body.sha.slice(0, 64) : null;
|
|
25
|
+
const service = inferService(body.service, files);
|
|
26
|
+
const inferredTags = inferTagsFromContext(files, prTitle);
|
|
27
|
+
const now = /* @__PURE__ */ new Date();
|
|
28
|
+
const dayOfWeek = now.getUTCDay();
|
|
29
|
+
const hourOfDay = now.getUTCHours();
|
|
30
|
+
const matchedRules = [];
|
|
31
|
+
const reasons = [];
|
|
32
|
+
let verdict = "pass";
|
|
33
|
+
for (const tag of inferredTags) {
|
|
34
|
+
const rules = await getStores().overrides.getRulesForTag(tag, service, req.tenantId);
|
|
35
|
+
for (const rule of rules) {
|
|
36
|
+
matchedRules.push(rule);
|
|
37
|
+
const timeMatch = await getStores().overrides.hasRecentOverride(tag, service, dayOfWeek, hourOfDay, req.tenantId);
|
|
38
|
+
const dayName = rule.dayOfWeek != null ? ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][rule.dayOfWeek] : null;
|
|
39
|
+
const hourDesc = rule.hourWindow ? `${rule.hourWindow[0]}:00\u2013${rule.hourWindow[1]}:00 UTC` : null;
|
|
40
|
+
if (rule.occurrences >= 3 && timeMatch) {
|
|
41
|
+
verdict = "block";
|
|
42
|
+
reasons.push(
|
|
43
|
+
`Corpus block: \`${tag}\` has been overridden ${rule.occurrences}\xD7 (reason: ${rule.overrideReason})` + (dayName ? ` \u2014 typically on ${dayName}` : "") + (hourDesc ? ` ${hourDesc}` : "") + ". This matches your current time window."
|
|
44
|
+
);
|
|
45
|
+
} else if (rule.occurrences >= 1) {
|
|
46
|
+
if (verdict === "pass") verdict = "warn";
|
|
47
|
+
reasons.push(
|
|
48
|
+
`Corpus warning: \`${tag}\` has been overridden ${rule.occurrences}\xD7 (reason: ${rule.overrideReason})` + (dayName ? ` \u2014 pattern clusters on ${dayName}` : "") + "."
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (diff) {
|
|
54
|
+
const semantic = analyzeSemanticRisk(diff);
|
|
55
|
+
if (semantic.risk === "high") {
|
|
56
|
+
verdict = "block";
|
|
57
|
+
reasons.push(semantic.reason ?? "Blocked: destructive operation detected in diff. Review and replace with a reversible alternative before merging.");
|
|
58
|
+
} else if (semantic.risk === "medium" && verdict === "pass") {
|
|
59
|
+
verdict = "warn";
|
|
60
|
+
reasons.push(`Semantic risk MEDIUM: ${semantic.reason ?? "elevated-risk operation in diff"}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const enterpriseResult = evaluateEnterprisePolicy({
|
|
64
|
+
files,
|
|
65
|
+
actor,
|
|
66
|
+
service,
|
|
67
|
+
timestamp: Date.now()
|
|
68
|
+
});
|
|
69
|
+
if (enterpriseResult.triggeredRules.length > 0) {
|
|
70
|
+
if (enterpriseResult.verdict === "block") {
|
|
71
|
+
verdict = "block";
|
|
72
|
+
} else if (enterpriseResult.verdict === "warn" && verdict !== "block") {
|
|
73
|
+
verdict = "warn";
|
|
74
|
+
}
|
|
75
|
+
reasons.push(...enterpriseResult.reasons);
|
|
76
|
+
}
|
|
77
|
+
let diffSizeReport = null;
|
|
78
|
+
if (diffStats) {
|
|
79
|
+
diffSizeReport = evaluateDiffSize(diffStats, { actorIsAi: isAiActor(actor) });
|
|
80
|
+
if (diffSizeReport.requiresApproval) {
|
|
81
|
+
if (verdict !== "block") verdict = "warn";
|
|
82
|
+
reasons.push(`Diff size HIGH (${diffSizeReport.score}/100): ${diffSizeReport.recommendation}`);
|
|
83
|
+
} else if (diffSizeReport.level === "MEDIUM" && verdict === "pass") {
|
|
84
|
+
verdict = "warn";
|
|
85
|
+
reasons.push(`Diff size MEDIUM (${diffSizeReport.score}/100): ${diffSizeReport.recommendation}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const BLAST_RADIUS_THRESHOLD = 3;
|
|
89
|
+
const downstreamCount = service !== "unknown" ? serviceGraph.getUpstreamImpact(service).length : 0;
|
|
90
|
+
let blastRadiusEscalated = false;
|
|
91
|
+
if (verdict === "warn" && downstreamCount > BLAST_RADIUS_THRESHOLD && isAiActor(actor)) {
|
|
92
|
+
verdict = "block";
|
|
93
|
+
blastRadiusEscalated = true;
|
|
94
|
+
reasons.push(
|
|
95
|
+
`Blast radius: ${downstreamCount} downstream service(s) depend on \`${service}\` and the change was made by an AI actor \u2014 escalated from WARN to BLOCK.`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (prTitle) {
|
|
99
|
+
const titleLower = prTitle.toLowerCase();
|
|
100
|
+
const summary = await getStores().overrides.getOverrideSummary(req.tenantId);
|
|
101
|
+
for (const entry of summary) {
|
|
102
|
+
if (titleLower.includes(entry.tag.replace(/_/g, " ")) || titleLower.includes(entry.tag.replace(/_/g, "-"))) {
|
|
103
|
+
if (verdict === "pass") verdict = "warn";
|
|
104
|
+
if (!reasons.some((r) => r.includes(entry.tag))) {
|
|
105
|
+
reasons.push(`PR title matches known override pattern: \`${entry.tag}\` (${entry.total} historical overrides)`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const riskScore = computeRiskScore(verdict, matchedRules);
|
|
111
|
+
const recommendation = verdict === "block" ? "Review the override corpus before merging. This change touches areas your team has explicitly blocked in the past." : verdict === "warn" ? "Proceed with caution \u2014 this change touches areas with a history of operational overrides." : "No corpus conflicts found. Safe to merge based on execution history.";
|
|
112
|
+
logger.info({ verdict, riskScore, tags: inferredTags, service, actor, files: files.length }, "ci-gate: evaluated PR");
|
|
113
|
+
const lineHits = diff ? _linkDiffToDecisions(diff, matchedRules, reasons) : [];
|
|
114
|
+
await getStores().gateHistory.record(
|
|
115
|
+
{ prNumber, repo, service, actor, verdict, riskScore, reasons, sha },
|
|
116
|
+
req.tenantId
|
|
117
|
+
);
|
|
118
|
+
res.json({
|
|
119
|
+
ok: true,
|
|
120
|
+
verdict,
|
|
121
|
+
riskScore,
|
|
122
|
+
reasons,
|
|
123
|
+
recommendation,
|
|
124
|
+
corpusMatches: matchedRules,
|
|
125
|
+
lineHits,
|
|
126
|
+
diffSize: diffSizeReport,
|
|
127
|
+
meta: {
|
|
128
|
+
service,
|
|
129
|
+
inferredTags,
|
|
130
|
+
filesChecked: files.length,
|
|
131
|
+
actor,
|
|
132
|
+
blastRadius: { downstreamCount, threshold: BLAST_RADIUS_THRESHOLD, escalated: blastRadiusEscalated }
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
return router;
|
|
137
|
+
}
|
|
138
|
+
function inferService(explicit, files) {
|
|
139
|
+
if (explicit && explicit.trim()) return explicit.trim();
|
|
140
|
+
for (const file of files) {
|
|
141
|
+
const match = file.match(/^(?:apps|services|packages|src)\/([^/]+)\//);
|
|
142
|
+
if (match) return match[1];
|
|
143
|
+
}
|
|
144
|
+
return "unknown";
|
|
145
|
+
}
|
|
146
|
+
function inferTagsFromContext(files, prTitle) {
|
|
147
|
+
const tags = /* @__PURE__ */ new Set();
|
|
148
|
+
const dbFiles = files.some((f) => /db|database|migration|pool|sequelize|prisma|knex|typeorm/i.test(f));
|
|
149
|
+
if (dbFiles) {
|
|
150
|
+
tags.add("infra_db_connection_pool");
|
|
151
|
+
tags.add("n_plus_one_query");
|
|
152
|
+
}
|
|
153
|
+
if (files.some((f) => /auth|jwt|session|token|oauth|passport/i.test(f))) {
|
|
154
|
+
tags.add("auth_token_not_persisted");
|
|
155
|
+
}
|
|
156
|
+
if (files.some((f) => /cache|redis|memcache|memory|heap/i.test(f))) {
|
|
157
|
+
tags.add("oom_kill");
|
|
158
|
+
}
|
|
159
|
+
if (files.some((f) => /rate.?limit|throttl/i.test(f))) {
|
|
160
|
+
tags.add("rate_limit_cascade");
|
|
161
|
+
}
|
|
162
|
+
if (files.some((f) => /deploy|dockerfile|k8s|helm|\.yml|\.yaml|\.tf/i.test(f))) {
|
|
163
|
+
tags.add("deploy_config_drift");
|
|
164
|
+
}
|
|
165
|
+
const titleLower = prTitle.toLowerCase();
|
|
166
|
+
if (/pool|connection/.test(titleLower)) tags.add("infra_db_connection_pool");
|
|
167
|
+
if (/auth|token|session/.test(titleLower)) tags.add("auth_token_not_persisted");
|
|
168
|
+
if (/memory|oom|heap/.test(titleLower)) tags.add("oom_kill");
|
|
169
|
+
if (/rate.?limit|throttl/.test(titleLower)) tags.add("rate_limit_cascade");
|
|
170
|
+
if (/deploy|rollout|release/.test(titleLower)) tags.add("deploy_config_drift");
|
|
171
|
+
try {
|
|
172
|
+
const tagStats = postmortemStore.tagStats();
|
|
173
|
+
for (const { tag } of tagStats.slice(0, 20)) {
|
|
174
|
+
const tagSlug = tag.replace(/^infra_/, "").replace(/_/g, "");
|
|
175
|
+
if (files.some((f) => f.toLowerCase().replace(/[^a-z]/g, "").includes(tagSlug))) {
|
|
176
|
+
tags.add(tag);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
}
|
|
181
|
+
return [...tags];
|
|
182
|
+
}
|
|
183
|
+
function computeRiskScore(verdict, rules) {
|
|
184
|
+
if (verdict === "block") return Math.min(100, 70 + rules.length * 5);
|
|
185
|
+
if (verdict === "warn") return Math.min(69, 30 + rules.length * 10);
|
|
186
|
+
return Math.max(0, 10 - rules.length * 2);
|
|
187
|
+
}
|
|
188
|
+
function _linkDiffToDecisions(diff, matchedRules, reasons) {
|
|
189
|
+
const lines = diff.split("\n");
|
|
190
|
+
const hits = [];
|
|
191
|
+
const patterns = [];
|
|
192
|
+
for (const rule of matchedRules) {
|
|
193
|
+
const tag = rule.incidentTag ?? "";
|
|
194
|
+
const slug = tag.replace(/_/g, "[-_\\s]").replace(/\//g, "\\/");
|
|
195
|
+
if (slug) patterns.push({ pattern: new RegExp(slug, "i"), reason: `Corpus rule: ${tag} (${rule.overrideReason})` });
|
|
196
|
+
}
|
|
197
|
+
const semanticKeywords = [
|
|
198
|
+
{ re: /DROP\s+TABLE|TRUNCATE|DELETE\s+FROM(?!\s+\w+\s+WHERE)/i, reason: "Destructive SQL operation" },
|
|
199
|
+
{ re: /terraform\s+destroy|aws\s+s3\s+rm/i, reason: "Destructive infrastructure command" },
|
|
200
|
+
{ re: /rm\s+-rf/i, reason: "Destructive filesystem command" },
|
|
201
|
+
{ re: /ALTER\s+TABLE/i, reason: "Schema mutation" },
|
|
202
|
+
{ re: /process\.env\.\w*(SECRET|KEY|TOKEN|PASS|PWD)/i, reason: "Potential secret exposure" },
|
|
203
|
+
{ re: /kubectl\s+delete\s+namespace/i, reason: "Cluster-scope kubectl delete" }
|
|
204
|
+
];
|
|
205
|
+
for (const kw of semanticKeywords) {
|
|
206
|
+
patterns.push({ pattern: kw.re, reason: kw.reason });
|
|
207
|
+
}
|
|
208
|
+
let lineNumber = 0;
|
|
209
|
+
for (const line of lines) {
|
|
210
|
+
lineNumber++;
|
|
211
|
+
if (!line.startsWith("+") || line.startsWith("+++")) continue;
|
|
212
|
+
const content = line.slice(1);
|
|
213
|
+
for (const { pattern, reason } of patterns) {
|
|
214
|
+
if (pattern.test(content)) {
|
|
215
|
+
if (!hits.some((h) => h.lineNumber === lineNumber)) {
|
|
216
|
+
hits.push({
|
|
217
|
+
lineNumber,
|
|
218
|
+
lineContent: content.slice(0, 200).trim(),
|
|
219
|
+
matchedPattern: pattern.source.slice(0, 80),
|
|
220
|
+
reason
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (hits.length >= 20) break;
|
|
227
|
+
}
|
|
228
|
+
return hits;
|
|
229
|
+
}
|
|
230
|
+
export {
|
|
231
|
+
createCIGateRouter
|
|
232
|
+
};
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { store } from "../sensor/buffer.js";
|
|
3
|
+
import logger from "../sensor/logger.js";
|
|
4
|
+
function createCIRouter() {
|
|
5
|
+
const router = Router();
|
|
6
|
+
router.post("/ci/github", (req, res) => {
|
|
7
|
+
try {
|
|
8
|
+
const body = req.body;
|
|
9
|
+
const eventType = req.headers["x-github-event"];
|
|
10
|
+
let event = null;
|
|
11
|
+
if (eventType === "workflow_run" || body.workflow_run) {
|
|
12
|
+
const wr = body.workflow_run ?? body;
|
|
13
|
+
const sha = String(wr.head_sha ?? wr.sha ?? "");
|
|
14
|
+
event = {
|
|
15
|
+
type: "ci",
|
|
16
|
+
provider: "github_actions",
|
|
17
|
+
sha,
|
|
18
|
+
shortSha: sha.slice(0, 7),
|
|
19
|
+
branch: String(wr.head_branch ?? wr.branch ?? ""),
|
|
20
|
+
workflow: String(wr.name ?? wr.workflow ?? "workflow"),
|
|
21
|
+
job: String(wr.name ?? "run"),
|
|
22
|
+
status: normalizeGitHubStatus(String(wr.conclusion ?? wr.status ?? "unknown")),
|
|
23
|
+
durationMs: computeGitHubDuration(wr),
|
|
24
|
+
url: String(wr.html_url ?? wr.url ?? ""),
|
|
25
|
+
failedTests: [],
|
|
26
|
+
timestamp: parseGitHubTs(wr.updated_at ?? wr.created_at) ?? Date.now()
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (eventType === "check_run" || body.check_run) {
|
|
30
|
+
const cr = body.check_run ?? body;
|
|
31
|
+
const sha = String(cr.head_sha ?? "");
|
|
32
|
+
event = {
|
|
33
|
+
type: "ci",
|
|
34
|
+
provider: "github_actions",
|
|
35
|
+
sha,
|
|
36
|
+
shortSha: sha.slice(0, 7),
|
|
37
|
+
branch: "",
|
|
38
|
+
workflow: String(cr.app?.name ?? "GitHub"),
|
|
39
|
+
job: String(cr.name ?? "check"),
|
|
40
|
+
status: normalizeGitHubStatus(String(cr.conclusion ?? cr.status ?? "unknown")),
|
|
41
|
+
durationMs: computeGitHubDuration(cr),
|
|
42
|
+
url: String(cr.html_url ?? ""),
|
|
43
|
+
failedTests: [],
|
|
44
|
+
timestamp: parseGitHubTs(cr.completed_at ?? cr.started_at) ?? Date.now()
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (!event && body.sha) {
|
|
48
|
+
const sha = String(body.sha);
|
|
49
|
+
event = {
|
|
50
|
+
type: "ci",
|
|
51
|
+
provider: "github_actions",
|
|
52
|
+
sha,
|
|
53
|
+
shortSha: sha.slice(0, 7),
|
|
54
|
+
branch: String(body.branch ?? ""),
|
|
55
|
+
workflow: String(body.workflow ?? "workflow"),
|
|
56
|
+
job: String(body.job ?? "job"),
|
|
57
|
+
status: normalizeGitHubStatus(String(body.status ?? "unknown")),
|
|
58
|
+
durationMs: typeof body.duration_ms === "number" ? body.duration_ms : void 0,
|
|
59
|
+
url: String(body.url ?? body.run_url ?? ""),
|
|
60
|
+
failedTests: parseFailedTests(body.failed_tests),
|
|
61
|
+
timestamp: Date.now()
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (!event) {
|
|
65
|
+
res.status(400).json({ error: "unrecognised GitHub event shape" });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
store.push(event);
|
|
69
|
+
logger.info({ sha: event.sha.slice(0, 7), status: event.status, job: event.job }, "ci: github event ingested");
|
|
70
|
+
res.json({ ok: true, sha: event.sha, status: event.status });
|
|
71
|
+
} catch (err) {
|
|
72
|
+
logger.warn({ err }, "ci/github: parse error");
|
|
73
|
+
res.status(400).json({ error: "parse error" });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
router.post("/ci/gitlab", (req, res) => {
|
|
77
|
+
try {
|
|
78
|
+
const body = req.body;
|
|
79
|
+
const sha = String(body.sha ?? body.checkout_sha ?? "");
|
|
80
|
+
const statusRaw = String(body.status ?? body.build_status ?? "unknown");
|
|
81
|
+
const event = {
|
|
82
|
+
type: "ci",
|
|
83
|
+
provider: "gitlab_ci",
|
|
84
|
+
sha,
|
|
85
|
+
shortSha: sha.slice(0, 7),
|
|
86
|
+
branch: String(body.ref ?? body.branch ?? "").replace("refs/heads/", ""),
|
|
87
|
+
workflow: String(body.pipeline?.name ?? body.project?.name ?? "pipeline"),
|
|
88
|
+
job: String(body.build_name ?? body.name ?? "job"),
|
|
89
|
+
status: normalizeGitLabStatus(statusRaw),
|
|
90
|
+
url: String(body.build_url ?? body.pipeline?.url ?? ""),
|
|
91
|
+
failedTests: parseFailedTests(body.failed_tests),
|
|
92
|
+
timestamp: Date.now()
|
|
93
|
+
};
|
|
94
|
+
store.push(event);
|
|
95
|
+
res.json({ ok: true, sha: event.sha, status: event.status });
|
|
96
|
+
} catch (err) {
|
|
97
|
+
logger.warn({ err }, "ci/gitlab: parse error");
|
|
98
|
+
res.status(400).json({ error: "parse error" });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
router.post("/ci/azure-devops", (req, res) => {
|
|
102
|
+
try {
|
|
103
|
+
const body = req.body;
|
|
104
|
+
const resource = body.resource ?? body;
|
|
105
|
+
const eventType = String(body.eventType ?? body.event ?? "");
|
|
106
|
+
if (eventType && eventType !== "build.complete" && eventType !== "ms.vss-build.build-completed-event") {
|
|
107
|
+
res.json({ ok: true, skipped: true, reason: `eventType ${eventType} not processed` });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const sha = String(resource.sourceVersion ?? resource.sourceVersionId ?? resource.commitId ?? "");
|
|
111
|
+
const definition = resource.definition ?? resource.plan ?? {};
|
|
112
|
+
const project = resource.project ?? body.resourceContainers?.project ?? {};
|
|
113
|
+
const links = resource._links ?? {};
|
|
114
|
+
const resultRaw = String(resource.result ?? resource.buildResult ?? "unknown").toLowerCase();
|
|
115
|
+
const status = resultRaw === "succeeded" ? "success" : resultRaw === "failed" || resultRaw === "partiallySucceeded" ? "failure" : resultRaw === "canceled" || resultRaw === "cancelled" ? "cancelled" : "failure";
|
|
116
|
+
const startMs = resource.startTime ? new Date(String(resource.startTime)).getTime() : null;
|
|
117
|
+
const finishMs = resource.finishTime ? new Date(String(resource.finishTime)).getTime() : null;
|
|
118
|
+
const durationMs = startMs && finishMs && finishMs > startMs ? finishMs - startMs : void 0;
|
|
119
|
+
const event = {
|
|
120
|
+
type: "ci",
|
|
121
|
+
provider: "azure_devops",
|
|
122
|
+
sha,
|
|
123
|
+
shortSha: sha.slice(0, 7),
|
|
124
|
+
branch: String(resource.sourceBranch ?? resource.branch ?? "").replace("refs/heads/", ""),
|
|
125
|
+
workflow: String(project.name ?? definition.name ?? "pipeline"),
|
|
126
|
+
job: String(resource.buildNumber ?? definition.name ?? "build"),
|
|
127
|
+
status,
|
|
128
|
+
durationMs,
|
|
129
|
+
url: links.web?.href ?? String(resource.url ?? resource.buildUrl ?? ""),
|
|
130
|
+
failedTests: [],
|
|
131
|
+
timestamp: finishMs ?? Date.now()
|
|
132
|
+
};
|
|
133
|
+
store.push(event);
|
|
134
|
+
logger.info({ sha: event.sha.slice(0, 7), status: event.status }, "ci/azure-devops: event ingested");
|
|
135
|
+
res.json({ ok: true, sha: event.sha, status: event.status });
|
|
136
|
+
} catch (err) {
|
|
137
|
+
logger.warn({ err }, "ci/azure-devops: parse error");
|
|
138
|
+
res.status(400).json({ error: "parse error" });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
router.post("/ci/jenkins", (req, res) => {
|
|
142
|
+
try {
|
|
143
|
+
const body = req.body;
|
|
144
|
+
const build = body.build ?? body;
|
|
145
|
+
const scm = build.scm ?? {};
|
|
146
|
+
const phase = String(build.phase ?? "").toUpperCase();
|
|
147
|
+
if (phase && phase !== "COMPLETED" && phase !== "FINISHED") {
|
|
148
|
+
res.json({ ok: true, skipped: true, reason: `phase ${phase} not processed` });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const sha = String(scm.commit ?? scm.sha ?? build.sha ?? "");
|
|
152
|
+
const statusRaw = String(build.status ?? build.result ?? "UNKNOWN").toUpperCase();
|
|
153
|
+
const status = statusRaw === "SUCCESS" ? "success" : statusRaw === "FAILURE" || statusRaw === "FAILED" ? "failure" : statusRaw === "ABORTED" ? "cancelled" : statusRaw === "UNSTABLE" ? "failure" : "failure";
|
|
154
|
+
const event = {
|
|
155
|
+
type: "ci",
|
|
156
|
+
provider: "jenkins",
|
|
157
|
+
sha,
|
|
158
|
+
shortSha: sha.slice(0, 7),
|
|
159
|
+
branch: String(scm.branch ?? build.branch ?? "").replace("refs/heads/", ""),
|
|
160
|
+
workflow: String(body.name ?? body.job ?? "pipeline"),
|
|
161
|
+
job: `${body.name ?? "build"} #${build.number ?? "?"}`,
|
|
162
|
+
status,
|
|
163
|
+
durationMs: typeof build.duration === "number" ? build.duration : void 0,
|
|
164
|
+
url: String(build.full_url ?? build.url ?? ""),
|
|
165
|
+
failedTests: parseFailedTests(build.test_summary ?? build.failedTests),
|
|
166
|
+
timestamp: Date.now()
|
|
167
|
+
};
|
|
168
|
+
store.push(event);
|
|
169
|
+
logger.info({ sha: event.sha.slice(0, 7), status: event.status }, "ci/jenkins: event ingested");
|
|
170
|
+
res.json({ ok: true, sha: event.sha, status: event.status });
|
|
171
|
+
} catch (err) {
|
|
172
|
+
logger.warn({ err }, "ci/jenkins: parse error");
|
|
173
|
+
res.status(400).json({ error: "parse error" });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
router.post("/ci/generic", (req, res) => {
|
|
177
|
+
try {
|
|
178
|
+
const body = req.body;
|
|
179
|
+
if (!body.sha || !body.status) {
|
|
180
|
+
res.status(400).json({ error: "sha and status are required" });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const sha = String(body.sha);
|
|
184
|
+
const event = {
|
|
185
|
+
type: "ci",
|
|
186
|
+
provider: body.provider ?? "unknown",
|
|
187
|
+
sha,
|
|
188
|
+
shortSha: sha.slice(0, 7),
|
|
189
|
+
branch: String(body.branch ?? ""),
|
|
190
|
+
workflow: String(body.workflow ?? ""),
|
|
191
|
+
job: String(body.job ?? "job"),
|
|
192
|
+
status: normalizeGenericStatus(String(body.status)),
|
|
193
|
+
durationMs: typeof body.duration_ms === "number" ? body.duration_ms : void 0,
|
|
194
|
+
url: String(body.url ?? ""),
|
|
195
|
+
failedTests: parseFailedTests(body.failed_tests),
|
|
196
|
+
timestamp: Date.now()
|
|
197
|
+
};
|
|
198
|
+
store.push(event);
|
|
199
|
+
res.json({ ok: true, sha: event.sha, status: event.status });
|
|
200
|
+
} catch (err) {
|
|
201
|
+
logger.warn({ err }, "ci/generic: parse error");
|
|
202
|
+
res.status(400).json({ error: "parse error" });
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
router.post("/deployments", (req, res) => {
|
|
206
|
+
try {
|
|
207
|
+
const body = req.body;
|
|
208
|
+
if (!body.sha || !body.environment || !body.status) {
|
|
209
|
+
res.status(400).json({ error: "sha, environment, and status are required" });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const sha = String(body.sha);
|
|
213
|
+
const event = {
|
|
214
|
+
type: "deployment",
|
|
215
|
+
environment: String(body.environment),
|
|
216
|
+
sha,
|
|
217
|
+
shortSha: sha.slice(0, 7),
|
|
218
|
+
version: String(body.version ?? body.tag ?? ""),
|
|
219
|
+
service: String(body.service ?? body.app ?? ""),
|
|
220
|
+
status: normalizeDeployStatus(String(body.status)),
|
|
221
|
+
url: String(body.url ?? ""),
|
|
222
|
+
actor: String(body.actor ?? body.deployer ?? ""),
|
|
223
|
+
timestamp: Date.now()
|
|
224
|
+
};
|
|
225
|
+
store.push(event);
|
|
226
|
+
logger.info(
|
|
227
|
+
{ sha: event.sha.slice(0, 7), env: event.environment, status: event.status },
|
|
228
|
+
"ci: deployment event ingested"
|
|
229
|
+
);
|
|
230
|
+
res.json({ ok: true, sha: event.sha, environment: event.environment, status: event.status });
|
|
231
|
+
} catch (err) {
|
|
232
|
+
logger.warn({ err }, "deployments: parse error");
|
|
233
|
+
res.status(400).json({ error: "parse error" });
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
return router;
|
|
237
|
+
}
|
|
238
|
+
function normalizeGitHubStatus(s) {
|
|
239
|
+
if (s === "success") return "success";
|
|
240
|
+
if (s === "failure" || s === "timed_out" || s === "startup_failure") return "failure";
|
|
241
|
+
if (s === "cancelled") return "cancelled";
|
|
242
|
+
if (s === "skipped" || s === "neutral") return "skipped";
|
|
243
|
+
return "failure";
|
|
244
|
+
}
|
|
245
|
+
function normalizeGitLabStatus(s) {
|
|
246
|
+
if (s === "success" || s === "passed") return "success";
|
|
247
|
+
if (s === "failed") return "failure";
|
|
248
|
+
if (s === "canceled") return "cancelled";
|
|
249
|
+
if (s === "skipped") return "skipped";
|
|
250
|
+
return "failure";
|
|
251
|
+
}
|
|
252
|
+
function normalizeGenericStatus(s) {
|
|
253
|
+
const lower = s.toLowerCase();
|
|
254
|
+
if (lower === "success" || lower === "pass" || lower === "passed") return "success";
|
|
255
|
+
if (lower === "failure" || lower === "fail" || lower === "failed") return "failure";
|
|
256
|
+
if (lower === "cancelled" || lower === "canceled") return "cancelled";
|
|
257
|
+
if (lower === "skipped") return "skipped";
|
|
258
|
+
return "failure";
|
|
259
|
+
}
|
|
260
|
+
function normalizeDeployStatus(s) {
|
|
261
|
+
const lower = s.toLowerCase();
|
|
262
|
+
if (lower === "started" || lower === "in_progress") return "started";
|
|
263
|
+
if (lower === "success" || lower === "deployed") return "success";
|
|
264
|
+
if (lower === "failure" || lower === "failed") return "failure";
|
|
265
|
+
if (lower === "rollback") return "rollback";
|
|
266
|
+
return "started";
|
|
267
|
+
}
|
|
268
|
+
function computeGitHubDuration(obj) {
|
|
269
|
+
const start = parseGitHubTs(obj.run_started_at ?? obj.started_at ?? obj.created_at);
|
|
270
|
+
const end = parseGitHubTs(obj.updated_at ?? obj.completed_at);
|
|
271
|
+
if (start && end && end > start) return end - start;
|
|
272
|
+
return void 0;
|
|
273
|
+
}
|
|
274
|
+
function parseGitHubTs(val) {
|
|
275
|
+
if (!val) return null;
|
|
276
|
+
const d = new Date(String(val));
|
|
277
|
+
return isNaN(d.getTime()) ? null : d.getTime();
|
|
278
|
+
}
|
|
279
|
+
function parseFailedTests(val) {
|
|
280
|
+
if (!Array.isArray(val)) return [];
|
|
281
|
+
return val.slice(0, 50).map((t) => {
|
|
282
|
+
if (typeof t === "string") return { name: t };
|
|
283
|
+
const obj = t;
|
|
284
|
+
return {
|
|
285
|
+
name: String(obj.name ?? obj.title ?? "unknown"),
|
|
286
|
+
error: obj.error ? String(obj.error) : void 0,
|
|
287
|
+
file: obj.file ? String(obj.file) : void 0
|
|
288
|
+
};
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
export {
|
|
292
|
+
createCIRouter
|
|
293
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { buildComplianceReport, renderComplianceHtml } from "../intelligence/compliance-report.js";
|
|
3
|
+
function createComplianceReportRouter() {
|
|
4
|
+
const router = Router();
|
|
5
|
+
router.get("/compliance/report", async (req, res) => {
|
|
6
|
+
const now = Date.now();
|
|
7
|
+
const format = (req.query.format ?? "json").toLowerCase();
|
|
8
|
+
const from = Number(req.query.from ?? now - 30 * 24 * 60 * 60 * 1e3);
|
|
9
|
+
const to = Number(req.query.to ?? now);
|
|
10
|
+
const report = await buildComplianceReport(from, to);
|
|
11
|
+
if (format === "html") {
|
|
12
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
13
|
+
res.setHeader("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'");
|
|
14
|
+
res.setHeader("Cache-Control", "no-store");
|
|
15
|
+
res.send(renderComplianceHtml(report));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
res.json({ ok: true, report });
|
|
19
|
+
});
|
|
20
|
+
return router;
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
createComplianceReportRouter
|
|
24
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { confidenceStore, ConfidenceReportSchema } from "../intelligence/confidence-report.js";
|
|
3
|
+
function createConfidenceRouter() {
|
|
4
|
+
const router = Router();
|
|
5
|
+
router.post("/confidence-reports", (req, res) => {
|
|
6
|
+
const parsed = ConfidenceReportSchema.safeParse(req.body);
|
|
7
|
+
if (!parsed.success) {
|
|
8
|
+
res.status(400).json({ error: "validation failed", details: parsed.error.issues });
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const report = confidenceStore.add(parsed.data);
|
|
12
|
+
res.status(201).json({ ok: true, report });
|
|
13
|
+
});
|
|
14
|
+
router.get("/confidence-reports", (req, res) => {
|
|
15
|
+
const limit = Math.min(100, Math.max(1, Number(req.query.limit ?? 20)));
|
|
16
|
+
res.json({ ok: true, reports: confidenceStore.list(limit) });
|
|
17
|
+
});
|
|
18
|
+
router.get("/confidence-reports/:id", (req, res) => {
|
|
19
|
+
const report = confidenceStore.get(req.params.id);
|
|
20
|
+
if (!report) {
|
|
21
|
+
res.status(404).json({ error: "report not found" });
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
res.json({ ok: true, report });
|
|
25
|
+
});
|
|
26
|
+
return router;
|
|
27
|
+
}
|
|
28
|
+
export {
|
|
29
|
+
createConfidenceRouter
|
|
30
|
+
};
|