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,74 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { SESSIONS_DIR } from "./paths.js";
|
|
4
|
+
import logger from "./logger.js";
|
|
5
|
+
const SCHEMA_VERSION = 1;
|
|
6
|
+
const MAX_SESSIONS = 30;
|
|
7
|
+
function ensureDir() {
|
|
8
|
+
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
function toSafeFilename(ts, label) {
|
|
11
|
+
const safe = label.replace(/[^a-zA-Z0-9_-]/g, "-").slice(0, 40);
|
|
12
|
+
return `${new Date(ts).toISOString().replace(/[:.]/g, "-")}-${safe}.json`;
|
|
13
|
+
}
|
|
14
|
+
function saveSessionToHistory(events, label = "session") {
|
|
15
|
+
if (events.length === 0) return;
|
|
16
|
+
try {
|
|
17
|
+
ensureDir();
|
|
18
|
+
const ts = Date.now();
|
|
19
|
+
const filename = toSafeFilename(ts, label);
|
|
20
|
+
const payload = { v: SCHEMA_VERSION, savedAt: ts, label, eventCount: events.length, events };
|
|
21
|
+
fs.writeFileSync(path.join(SESSIONS_DIR, filename), JSON.stringify(payload), "utf8");
|
|
22
|
+
pruneOldSessions();
|
|
23
|
+
logger.info({ count: events.length, filename }, "session saved to history");
|
|
24
|
+
} catch (err) {
|
|
25
|
+
logger.warn({ err }, "session history save failed");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function listFiles() {
|
|
29
|
+
try {
|
|
30
|
+
ensureDir();
|
|
31
|
+
return fs.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json")).sort();
|
|
32
|
+
} catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function pruneOldSessions() {
|
|
37
|
+
const files = listFiles();
|
|
38
|
+
if (files.length <= MAX_SESSIONS) return;
|
|
39
|
+
for (const f of files.slice(0, files.length - MAX_SESSIONS)) {
|
|
40
|
+
try {
|
|
41
|
+
fs.unlinkSync(path.join(SESSIONS_DIR, f));
|
|
42
|
+
} catch {
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function listSessionMetas() {
|
|
47
|
+
return listFiles().map((filename) => {
|
|
48
|
+
try {
|
|
49
|
+
const raw = fs.readFileSync(path.join(SESSIONS_DIR, filename), "utf8");
|
|
50
|
+
const parsed = JSON.parse(raw);
|
|
51
|
+
return { id: filename.replace(".json", ""), savedAt: parsed.savedAt, eventCount: parsed.eventCount, label: parsed.label, filename };
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}).filter((m) => m !== null).reverse();
|
|
56
|
+
}
|
|
57
|
+
function loadSessionsByTimeRange(since, until) {
|
|
58
|
+
const metas = listSessionMetas().filter((m) => m.savedAt >= since && m.savedAt <= until);
|
|
59
|
+
const all = [];
|
|
60
|
+
for (const meta of metas) {
|
|
61
|
+
try {
|
|
62
|
+
const raw = fs.readFileSync(path.join(SESSIONS_DIR, meta.filename), "utf8");
|
|
63
|
+
const parsed = JSON.parse(raw);
|
|
64
|
+
if (parsed.v === SCHEMA_VERSION) all.push(...parsed.events);
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return all.sort((a, b) => a.timestamp - b.timestamp);
|
|
69
|
+
}
|
|
70
|
+
export {
|
|
71
|
+
listSessionMetas,
|
|
72
|
+
loadSessionsByTimeRange,
|
|
73
|
+
saveSessionToHistory
|
|
74
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { SESSION_FILE, DATA_DIR } from "./paths.js";
|
|
3
|
+
import logger from "./logger.js";
|
|
4
|
+
const SCHEMA_VERSION = 1;
|
|
5
|
+
const SESSION_MAX_AGE_MS = 8 * 60 * 60 * 1e3;
|
|
6
|
+
function saveSession(events) {
|
|
7
|
+
if (events.length === 0) return;
|
|
8
|
+
try {
|
|
9
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
10
|
+
const payload = { v: SCHEMA_VERSION, savedAt: Date.now(), events };
|
|
11
|
+
fs.writeFileSync(SESSION_FILE, JSON.stringify(payload), "utf8");
|
|
12
|
+
logger.info({ count: events.length }, "session persisted to disk");
|
|
13
|
+
} catch (err) {
|
|
14
|
+
logger.warn({ err }, "session persist failed \u2014 buffer will be lost on restart");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function loadSession() {
|
|
18
|
+
try {
|
|
19
|
+
if (!fs.existsSync(SESSION_FILE)) return null;
|
|
20
|
+
const raw = fs.readFileSync(SESSION_FILE, "utf8");
|
|
21
|
+
const parsed = JSON.parse(raw);
|
|
22
|
+
if (parsed.v !== SCHEMA_VERSION) return null;
|
|
23
|
+
const age = Date.now() - parsed.savedAt;
|
|
24
|
+
if (age > SESSION_MAX_AGE_MS) {
|
|
25
|
+
fs.unlinkSync(SESSION_FILE);
|
|
26
|
+
logger.info({ ageMs: age }, "stale session discarded");
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return parsed.events;
|
|
30
|
+
} catch (err) {
|
|
31
|
+
logger.warn({ err }, "session load failed");
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export {
|
|
36
|
+
loadSession,
|
|
37
|
+
saveSession
|
|
38
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { DATA_DIR, zeroRetentionMode } from "./paths.js";
|
|
4
|
+
import logger from "./logger.js";
|
|
5
|
+
import { stageProposal } from "../intelligence/policy-proposals.js";
|
|
6
|
+
import { loadEnterprisePolicy } from "../intelligence/enterprise-policy-engine.js";
|
|
7
|
+
const SHADOW_RULE_STATS_FILE = path.join(DATA_DIR, "shadow-rule-stats.json");
|
|
8
|
+
const MAX_STATS_ENTRIES = 1e3;
|
|
9
|
+
const PROMOTION_WINDOW_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
10
|
+
const MIN_HITS_FOR_PROMOTION = 5;
|
|
11
|
+
let _stats = {};
|
|
12
|
+
let _loaded = false;
|
|
13
|
+
function _load() {
|
|
14
|
+
if (_loaded) return;
|
|
15
|
+
_loaded = true;
|
|
16
|
+
if (!fs.existsSync(SHADOW_RULE_STATS_FILE)) return;
|
|
17
|
+
try {
|
|
18
|
+
const raw = JSON.parse(fs.readFileSync(SHADOW_RULE_STATS_FILE, "utf8"));
|
|
19
|
+
if (raw?.version === 1 && raw.stats) {
|
|
20
|
+
_stats = raw.stats;
|
|
21
|
+
}
|
|
22
|
+
} catch (err) {
|
|
23
|
+
logger.warn({ err }, "shadow-promote: failed to load stats \u2014 starting fresh");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function _persist() {
|
|
27
|
+
if (zeroRetentionMode()) return;
|
|
28
|
+
try {
|
|
29
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
30
|
+
const entries = Object.entries(_stats);
|
|
31
|
+
if (entries.length > MAX_STATS_ENTRIES) {
|
|
32
|
+
const sorted = entries.sort(([, a], [, b]) => b.lastHitAt - a.lastHitAt);
|
|
33
|
+
_stats = Object.fromEntries(sorted.slice(0, MAX_STATS_ENTRIES));
|
|
34
|
+
}
|
|
35
|
+
const tmp = `${SHADOW_RULE_STATS_FILE}.tmp.${process.pid}`;
|
|
36
|
+
fs.writeFileSync(tmp, JSON.stringify({ version: 1, stats: _stats }), "utf8");
|
|
37
|
+
fs.renameSync(tmp, SHADOW_RULE_STATS_FILE);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
logger.warn({ err }, "shadow-promote: persist failed");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function _ensureRule(ruleId, ruleName) {
|
|
43
|
+
if (!_stats[ruleId]) {
|
|
44
|
+
_stats[ruleId] = {
|
|
45
|
+
ruleId,
|
|
46
|
+
ruleName,
|
|
47
|
+
totalHits: 0,
|
|
48
|
+
falsePositives: 0,
|
|
49
|
+
confirmedHits: 0,
|
|
50
|
+
firstHitAt: Date.now(),
|
|
51
|
+
lastHitAt: Date.now(),
|
|
52
|
+
proposalStaged: false,
|
|
53
|
+
proposalStagedAt: null
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return _stats[ruleId];
|
|
57
|
+
}
|
|
58
|
+
function recordShadowRuleHit(ruleId, ruleName) {
|
|
59
|
+
_load();
|
|
60
|
+
const stat = _ensureRule(ruleId, ruleName);
|
|
61
|
+
stat.totalHits++;
|
|
62
|
+
stat.lastHitAt = Date.now();
|
|
63
|
+
_persist();
|
|
64
|
+
_maybeStagePromosal(stat);
|
|
65
|
+
}
|
|
66
|
+
function recordShadowRuleFeedback(ruleId, ruleName, isFalsePositive) {
|
|
67
|
+
_load();
|
|
68
|
+
const stat = _ensureRule(ruleId, ruleName);
|
|
69
|
+
if (isFalsePositive) {
|
|
70
|
+
stat.falsePositives++;
|
|
71
|
+
stat.proposalStaged = false;
|
|
72
|
+
stat.proposalStagedAt = null;
|
|
73
|
+
} else {
|
|
74
|
+
stat.confirmedHits++;
|
|
75
|
+
}
|
|
76
|
+
_persist();
|
|
77
|
+
}
|
|
78
|
+
function resetShadowRuleStats(ruleId) {
|
|
79
|
+
_load();
|
|
80
|
+
delete _stats[ruleId];
|
|
81
|
+
_persist();
|
|
82
|
+
}
|
|
83
|
+
function getShadowRuleStats() {
|
|
84
|
+
_load();
|
|
85
|
+
return Object.values(_stats).sort((a, b) => b.totalHits - a.totalHits);
|
|
86
|
+
}
|
|
87
|
+
function _maybeStagePromosal(stat) {
|
|
88
|
+
if (stat.proposalStaged) return;
|
|
89
|
+
if (stat.totalHits < MIN_HITS_FOR_PROMOTION) return;
|
|
90
|
+
if (stat.falsePositives > 0) return;
|
|
91
|
+
if (Date.now() - stat.firstHitAt < PROMOTION_WINDOW_MS) return;
|
|
92
|
+
const policy = loadEnterprisePolicy();
|
|
93
|
+
const rule = policy.rules.find((r) => r.id === stat.ruleId);
|
|
94
|
+
if (!rule || rule.action !== "warn") return;
|
|
95
|
+
const proposalRule = {
|
|
96
|
+
...rule,
|
|
97
|
+
action: "warn",
|
|
98
|
+
name: `[Shadow Promote] ${rule.name}`,
|
|
99
|
+
reason: `Shadow promotion: rule "${rule.name}" has fired ${stat.totalHits} times over ${Math.round((Date.now() - stat.firstHitAt) / 864e5)} days with zero false positives. Approve to promote to block.`
|
|
100
|
+
};
|
|
101
|
+
const ruleHash = `shadow_promote_${stat.ruleId}`;
|
|
102
|
+
try {
|
|
103
|
+
const proposal = stageProposal(ruleHash, proposalRule, stat.totalHits);
|
|
104
|
+
if (proposal) {
|
|
105
|
+
stat.proposalStaged = true;
|
|
106
|
+
stat.proposalStagedAt = Date.now();
|
|
107
|
+
_persist();
|
|
108
|
+
logger.info(
|
|
109
|
+
{ ruleId: stat.ruleId, hits: stat.totalHits },
|
|
110
|
+
"shadow-promote: staged promotion proposal for rule"
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
} catch (err) {
|
|
114
|
+
logger.warn({ err, ruleId: stat.ruleId }, "shadow-promote: failed to stage proposal");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export {
|
|
118
|
+
getShadowRuleStats,
|
|
119
|
+
recordShadowRuleFeedback,
|
|
120
|
+
recordShadowRuleHit,
|
|
121
|
+
resetShadowRuleStats
|
|
122
|
+
};
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { execFile } from "child_process";
|
|
4
|
+
import { promisify } from "util";
|
|
5
|
+
import { SourceMapConsumer } from "source-map";
|
|
6
|
+
import { LRUCache } from "lru-cache";
|
|
7
|
+
import fg from "fast-glob";
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
const consumerCache = new LRUCache({
|
|
10
|
+
max: 20,
|
|
11
|
+
dispose: (consumer) => {
|
|
12
|
+
try {
|
|
13
|
+
consumer.destroy();
|
|
14
|
+
} catch {
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
const MAP_INDEX_TTL_MS = 3e4;
|
|
19
|
+
let mapIndex = null;
|
|
20
|
+
let mapIndexCreatedAt = 0;
|
|
21
|
+
let scanInFlight = null;
|
|
22
|
+
async function getMapIndex() {
|
|
23
|
+
if (mapIndex && Date.now() - mapIndexCreatedAt < MAP_INDEX_TTL_MS) return mapIndex;
|
|
24
|
+
if (scanInFlight) return scanInFlight;
|
|
25
|
+
scanInFlight = (async () => {
|
|
26
|
+
const files = await fg("**/*.js.map", {
|
|
27
|
+
cwd: process.cwd(),
|
|
28
|
+
absolute: true,
|
|
29
|
+
ignore: ["**/node_modules/**", "**/.git/**"],
|
|
30
|
+
deep: 8
|
|
31
|
+
});
|
|
32
|
+
const index = /* @__PURE__ */ new Map();
|
|
33
|
+
for (const f of files) {
|
|
34
|
+
index.set(path.basename(f), f);
|
|
35
|
+
}
|
|
36
|
+
mapIndex = index;
|
|
37
|
+
mapIndexCreatedAt = Date.now();
|
|
38
|
+
scanInFlight = null;
|
|
39
|
+
return index;
|
|
40
|
+
})();
|
|
41
|
+
return scanInFlight;
|
|
42
|
+
}
|
|
43
|
+
const BLAME_TIMEOUT_MS = 800;
|
|
44
|
+
const MAX_BLAME_CACHE = 500;
|
|
45
|
+
const blameCache = /* @__PURE__ */ new Map();
|
|
46
|
+
async function blameFrame(file, line) {
|
|
47
|
+
const absFile = path.isAbsolute(file) ? file : path.resolve(process.cwd(), file);
|
|
48
|
+
const cacheKey = `${absFile}:${line}`;
|
|
49
|
+
if (blameCache.has(cacheKey)) return blameCache.get(cacheKey);
|
|
50
|
+
if (blameCache.size >= MAX_BLAME_CACHE) {
|
|
51
|
+
blameCache.delete(blameCache.keys().next().value);
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
await fs.access(absFile);
|
|
55
|
+
} catch {
|
|
56
|
+
blameCache.set(cacheKey, null);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const { stdout } = await execFileAsync(
|
|
61
|
+
"git",
|
|
62
|
+
["blame", "-L", `${line},${line}`, "--porcelain", absFile],
|
|
63
|
+
{ cwd: process.cwd(), timeout: BLAME_TIMEOUT_MS }
|
|
64
|
+
);
|
|
65
|
+
const lines = stdout.split("\n");
|
|
66
|
+
const sha = lines[0]?.slice(0, 40) ?? "";
|
|
67
|
+
if (!sha || /^0+$/.test(sha)) {
|
|
68
|
+
blameCache.set(cacheKey, null);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const author = lines.find((l) => l.startsWith("author "))?.slice(7).trim() ?? "unknown";
|
|
72
|
+
const summary = lines.find((l) => l.startsWith("summary "))?.slice(8).trim() ?? "";
|
|
73
|
+
const result = { sha, author, summary };
|
|
74
|
+
blameCache.set(cacheKey, result);
|
|
75
|
+
return result;
|
|
76
|
+
} catch {
|
|
77
|
+
blameCache.set(cacheKey, null);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function getConsumer(mapPath) {
|
|
82
|
+
const cached = consumerCache.get(mapPath);
|
|
83
|
+
if (cached) return cached;
|
|
84
|
+
const raw = await fs.readFile(mapPath, "utf8");
|
|
85
|
+
const consumer = await new SourceMapConsumer(raw);
|
|
86
|
+
consumerCache.set(mapPath, consumer);
|
|
87
|
+
return consumer;
|
|
88
|
+
}
|
|
89
|
+
const FRAME_RE = /^\s+at\s+(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?$/;
|
|
90
|
+
function extractSnippet(content, errorLine, context = 2) {
|
|
91
|
+
const lines = content.split("\n");
|
|
92
|
+
const start = Math.max(0, errorLine - 1 - context);
|
|
93
|
+
const end = Math.min(lines.length - 1, errorLine - 1 + context);
|
|
94
|
+
const width = String(end + 1).length;
|
|
95
|
+
const rows = [];
|
|
96
|
+
for (let i = start; i <= end; i++) {
|
|
97
|
+
const num = String(i + 1).padStart(width);
|
|
98
|
+
const isCrash = i + 1 === errorLine;
|
|
99
|
+
const pointer = isCrash ? "\u25B6" : " ";
|
|
100
|
+
const annotation = isCrash ? " // [ROOT CAUSE]" : "";
|
|
101
|
+
rows.push(` ${pointer} ${num} \u2502 ${lines[i]}${annotation}`);
|
|
102
|
+
}
|
|
103
|
+
return rows.join("\n");
|
|
104
|
+
}
|
|
105
|
+
async function deminifyFrame(frame) {
|
|
106
|
+
const match = FRAME_RE.exec(frame);
|
|
107
|
+
if (!match) return frame;
|
|
108
|
+
const [, fnName, fileUrl, lineStr, colStr] = match;
|
|
109
|
+
const line = parseInt(lineStr, 10);
|
|
110
|
+
const column = parseInt(colStr, 10);
|
|
111
|
+
const mapName = path.basename(fileUrl.split("?")[0]) + ".map";
|
|
112
|
+
let index = await getMapIndex();
|
|
113
|
+
let mapPath = index.get(mapName);
|
|
114
|
+
if (!mapPath) {
|
|
115
|
+
mapIndex = null;
|
|
116
|
+
index = await getMapIndex();
|
|
117
|
+
mapPath = index.get(mapName);
|
|
118
|
+
if (!mapPath) return `${frame} [no sourcemap found]`;
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const consumer = await getConsumer(mapPath);
|
|
122
|
+
const pos = consumer.originalPositionFor({ line, column });
|
|
123
|
+
if (!pos.source) return `${frame} [no sourcemap found]`;
|
|
124
|
+
const fn = pos.name ?? fnName ?? "<anonymous>";
|
|
125
|
+
const resolved = ` at ${fn} (${pos.source}:${pos.line}:${pos.column})`;
|
|
126
|
+
try {
|
|
127
|
+
const sourceContent = consumer.sourceContentFor(pos.source, true);
|
|
128
|
+
if (sourceContent && pos.line) {
|
|
129
|
+
const snippet = extractSnippet(sourceContent, pos.line);
|
|
130
|
+
return `${resolved}
|
|
131
|
+
${snippet}`;
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
}
|
|
135
|
+
return resolved;
|
|
136
|
+
} catch {
|
|
137
|
+
return `${frame} [sourcemap error]`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async function resolveStackTrace(stack) {
|
|
141
|
+
const lines = stack.split("\n");
|
|
142
|
+
const resolved = await Promise.all(lines.map(deminifyFrame));
|
|
143
|
+
return resolved.join("\n");
|
|
144
|
+
}
|
|
145
|
+
async function resolveFrameAndStack(stack) {
|
|
146
|
+
const lines = stack.split("\n");
|
|
147
|
+
let primaryFrame = null;
|
|
148
|
+
const resolvedLines = await Promise.all(
|
|
149
|
+
lines.map(async (line) => {
|
|
150
|
+
const match = FRAME_RE.exec(line);
|
|
151
|
+
if (!match) return line;
|
|
152
|
+
const [, fnName, fileUrl, lineStr, colStr] = match;
|
|
153
|
+
const lineNum = parseInt(lineStr, 10);
|
|
154
|
+
const colNum = parseInt(colStr, 10);
|
|
155
|
+
const mapName = path.basename(fileUrl.split("?")[0]) + ".map";
|
|
156
|
+
let index = await getMapIndex();
|
|
157
|
+
let mapPath = index.get(mapName);
|
|
158
|
+
if (!mapPath) {
|
|
159
|
+
mapIndex = null;
|
|
160
|
+
index = await getMapIndex();
|
|
161
|
+
mapPath = index.get(mapName);
|
|
162
|
+
if (!mapPath) return `${line} [no sourcemap found]`;
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
const consumer = await getConsumer(mapPath);
|
|
166
|
+
const pos = consumer.originalPositionFor({ line: lineNum, column: colNum });
|
|
167
|
+
if (!pos.source) return `${line} [no sourcemap found]`;
|
|
168
|
+
const fn = pos.name ?? fnName ?? "<anonymous>";
|
|
169
|
+
const resolved = ` at ${fn} (${pos.source}:${pos.line}:${pos.column})`;
|
|
170
|
+
let snippet = "";
|
|
171
|
+
try {
|
|
172
|
+
const sourceContent = consumer.sourceContentFor(pos.source, true);
|
|
173
|
+
if (sourceContent && pos.line) {
|
|
174
|
+
snippet = extractSnippet(sourceContent, pos.line);
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
}
|
|
178
|
+
return snippet ? `${resolved}
|
|
179
|
+
${snippet}` : resolved;
|
|
180
|
+
} catch {
|
|
181
|
+
return `${line} [sourcemap error]`;
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
);
|
|
185
|
+
for (let i = 0; i < lines.length; i++) {
|
|
186
|
+
if (primaryFrame) break;
|
|
187
|
+
const match = FRAME_RE.exec(lines[i]);
|
|
188
|
+
if (!match) continue;
|
|
189
|
+
const [, fnName, fileUrl, lineStr, colStr] = match;
|
|
190
|
+
if (fileUrl.startsWith("node:") || fileUrl.includes("node_modules")) continue;
|
|
191
|
+
const lineNum = parseInt(lineStr, 10);
|
|
192
|
+
const colNum = parseInt(colStr, 10);
|
|
193
|
+
const mapName = path.basename(fileUrl.split("?")[0]) + ".map";
|
|
194
|
+
const index = await getMapIndex();
|
|
195
|
+
const mapPath = index.get(mapName);
|
|
196
|
+
if (!mapPath) continue;
|
|
197
|
+
try {
|
|
198
|
+
const consumer = await getConsumer(mapPath);
|
|
199
|
+
const pos = consumer.originalPositionFor({ line: lineNum, column: colNum });
|
|
200
|
+
if (!pos.source || !pos.line) continue;
|
|
201
|
+
const fn = pos.name ?? fnName ?? "<anonymous>";
|
|
202
|
+
let snippet = "";
|
|
203
|
+
try {
|
|
204
|
+
const sourceContent = consumer.sourceContentFor(pos.source, true);
|
|
205
|
+
if (sourceContent) snippet = extractSnippet(sourceContent, pos.line, 5);
|
|
206
|
+
} catch {
|
|
207
|
+
}
|
|
208
|
+
let gitSuspect = null;
|
|
209
|
+
try {
|
|
210
|
+
gitSuspect = await blameFrame(pos.source, pos.line);
|
|
211
|
+
} catch {
|
|
212
|
+
}
|
|
213
|
+
primaryFrame = {
|
|
214
|
+
fn,
|
|
215
|
+
file: pos.source,
|
|
216
|
+
line: pos.line,
|
|
217
|
+
column: pos.column ?? colNum,
|
|
218
|
+
snippet,
|
|
219
|
+
rawResolved: ` at ${fn} (${pos.source}:${pos.line}:${pos.column})`,
|
|
220
|
+
gitSuspect
|
|
221
|
+
};
|
|
222
|
+
} catch {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return { primaryFrame, resolvedStack: resolvedLines.join("\n") };
|
|
227
|
+
}
|
|
228
|
+
async function resolveFirstFrame(stack) {
|
|
229
|
+
const lines = stack.split("\n");
|
|
230
|
+
for (const line of lines) {
|
|
231
|
+
const match = FRAME_RE.exec(line);
|
|
232
|
+
if (!match) continue;
|
|
233
|
+
const [, fnName, fileUrl, lineStr, colStr] = match;
|
|
234
|
+
if (fileUrl.startsWith("node:") || fileUrl.includes("node_modules")) continue;
|
|
235
|
+
const lineNum = parseInt(lineStr, 10);
|
|
236
|
+
const colNum = parseInt(colStr, 10);
|
|
237
|
+
const mapName = path.basename(fileUrl.split("?")[0]) + ".map";
|
|
238
|
+
let index = await getMapIndex();
|
|
239
|
+
let mapPath = index.get(mapName);
|
|
240
|
+
if (!mapPath) {
|
|
241
|
+
mapIndex = null;
|
|
242
|
+
index = await getMapIndex();
|
|
243
|
+
mapPath = index.get(mapName);
|
|
244
|
+
}
|
|
245
|
+
if (!mapPath) continue;
|
|
246
|
+
try {
|
|
247
|
+
const consumer = await getConsumer(mapPath);
|
|
248
|
+
const pos = consumer.originalPositionFor({ line: lineNum, column: colNum });
|
|
249
|
+
if (!pos.source || !pos.line) continue;
|
|
250
|
+
const fn = pos.name ?? fnName ?? "<anonymous>";
|
|
251
|
+
let snippet = "";
|
|
252
|
+
try {
|
|
253
|
+
const sourceContent = consumer.sourceContentFor(pos.source, true);
|
|
254
|
+
if (sourceContent) snippet = extractSnippet(sourceContent, pos.line, 5);
|
|
255
|
+
} catch {
|
|
256
|
+
}
|
|
257
|
+
let gitSuspect = null;
|
|
258
|
+
try {
|
|
259
|
+
gitSuspect = await blameFrame(pos.source, pos.line);
|
|
260
|
+
} catch {
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
fn,
|
|
264
|
+
file: pos.source,
|
|
265
|
+
line: pos.line,
|
|
266
|
+
column: pos.column ?? colNum,
|
|
267
|
+
snippet,
|
|
268
|
+
rawResolved: ` at ${fn} (${pos.source}:${pos.line}:${pos.column})`,
|
|
269
|
+
gitSuspect
|
|
270
|
+
};
|
|
271
|
+
} catch {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
export {
|
|
278
|
+
resolveFirstFrame,
|
|
279
|
+
resolveFrameAndStack,
|
|
280
|
+
resolveStackTrace
|
|
281
|
+
};
|