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,597 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { lockAndExecute } from "../sensor/file-lock.js";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { randomUUID } from "crypto";
|
|
5
|
+
import { OVERRIDE_CORPUS_FILE } from "../sensor/paths.js";
|
|
6
|
+
import logger from "../sensor/logger.js";
|
|
7
|
+
import { normalizeForMatching } from "./normalize.js";
|
|
8
|
+
const OVERRIDE_REASONS = [
|
|
9
|
+
"batch-window",
|
|
10
|
+
"cost-constraint",
|
|
11
|
+
"on-call-discretion",
|
|
12
|
+
"compliance-hold",
|
|
13
|
+
"prefer-read-replica",
|
|
14
|
+
"maintenance-window",
|
|
15
|
+
"wrong-diagnosis",
|
|
16
|
+
"wrong-fix",
|
|
17
|
+
"other"
|
|
18
|
+
];
|
|
19
|
+
const MAX_EVENTS = 2e3;
|
|
20
|
+
let _events = [];
|
|
21
|
+
let _loaded = false;
|
|
22
|
+
const READ_CACHE_TTL_MS = 2e3;
|
|
23
|
+
let _lastForcedLoadAt = 0;
|
|
24
|
+
let _compactedRules = null;
|
|
25
|
+
let _corpusDirty = true;
|
|
26
|
+
function load(force = false) {
|
|
27
|
+
if (_loaded && !force) return;
|
|
28
|
+
if (!fs.existsSync(OVERRIDE_CORPUS_FILE)) {
|
|
29
|
+
_events = [];
|
|
30
|
+
_loaded = true;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const raw = fs.readFileSync(OVERRIDE_CORPUS_FILE, "utf8");
|
|
35
|
+
const parsed = JSON.parse(raw);
|
|
36
|
+
if (parsed?.version === 1 && Array.isArray(parsed.events)) {
|
|
37
|
+
_events = parsed.events.slice(-MAX_EVENTS);
|
|
38
|
+
} else {
|
|
39
|
+
_events = [];
|
|
40
|
+
}
|
|
41
|
+
_loaded = true;
|
|
42
|
+
_lastForcedLoadAt = Date.now();
|
|
43
|
+
} catch (err) {
|
|
44
|
+
logger.warn({ err }, "override-corpus: failed to load \u2014 starting fresh");
|
|
45
|
+
_events = [];
|
|
46
|
+
_loaded = true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function loadForRead() {
|
|
50
|
+
if (!_loaded || Date.now() - _lastForcedLoadAt > READ_CACHE_TTL_MS) {
|
|
51
|
+
load(true);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
let _tmpCounter = 0;
|
|
55
|
+
function persist() {
|
|
56
|
+
try {
|
|
57
|
+
fs.mkdirSync(path.dirname(OVERRIDE_CORPUS_FILE), { recursive: true });
|
|
58
|
+
const payload = { version: 1, events: _events };
|
|
59
|
+
_tmpCounter = _tmpCounter + 1 >>> 0;
|
|
60
|
+
const tmp = `${OVERRIDE_CORPUS_FILE}.tmp.${process.pid}.${Date.now().toString(36)}.${_tmpCounter.toString(36)}`;
|
|
61
|
+
fs.writeFileSync(tmp, JSON.stringify(payload), "utf8");
|
|
62
|
+
fs.renameSync(tmp, OVERRIDE_CORPUS_FILE);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
logger.warn({ err }, "override-corpus: failed to persist");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const DEFAULT_EXPIRY_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
68
|
+
function detectConflicts(input) {
|
|
69
|
+
loadForRead();
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
const CONFLICT_PAIRS = [
|
|
72
|
+
["batch-window", "wrong-fix"],
|
|
73
|
+
["maintenance-window", "wrong-diagnosis"],
|
|
74
|
+
["compliance-hold", "on-call-discretion"]
|
|
75
|
+
];
|
|
76
|
+
const conflicts = [];
|
|
77
|
+
for (const ev of _events) {
|
|
78
|
+
if (ev.incidentTag !== input.incidentTag) continue;
|
|
79
|
+
if (ev.service !== input.service) continue;
|
|
80
|
+
if (ev.expiresAt && ev.expiresAt > 0 && ev.expiresAt < now) continue;
|
|
81
|
+
const commandOverlap = ev.proposedCommand.slice(0, 50).toLowerCase() === input.proposedCommand.slice(0, 50).toLowerCase();
|
|
82
|
+
if (commandOverlap) {
|
|
83
|
+
for (const [a, b] of CONFLICT_PAIRS) {
|
|
84
|
+
if (ev.overrideReason === a && input.overrideReason === b || ev.overrideReason === b && input.overrideReason === a) {
|
|
85
|
+
conflicts.push(ev.id);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return conflicts;
|
|
91
|
+
}
|
|
92
|
+
function recordOverride(input) {
|
|
93
|
+
const now = /* @__PURE__ */ new Date();
|
|
94
|
+
const conflicts = detectConflicts({
|
|
95
|
+
incidentTag: input.incidentTag,
|
|
96
|
+
service: input.service,
|
|
97
|
+
overrideReason: input.overrideReason,
|
|
98
|
+
proposedCommand: input.proposedCommand
|
|
99
|
+
});
|
|
100
|
+
const expiresAt = input.expiresAt === 0 ? 0 : input.expiresAt ?? now.getTime() + DEFAULT_EXPIRY_MS;
|
|
101
|
+
const event = {
|
|
102
|
+
...input,
|
|
103
|
+
id: randomUUID(),
|
|
104
|
+
proposedCommand: input.proposedCommand.slice(0, 500),
|
|
105
|
+
note: input.note?.slice(0, 200),
|
|
106
|
+
rationale: input.rationale?.slice(0, 300),
|
|
107
|
+
manualAction: input.manualAction?.slice(0, 500),
|
|
108
|
+
dayOfWeek: now.getUTCDay(),
|
|
109
|
+
hourOfDay: now.getUTCHours(),
|
|
110
|
+
recordedAt: now.getTime(),
|
|
111
|
+
expiresAt
|
|
112
|
+
};
|
|
113
|
+
return lockAndExecute(`${OVERRIDE_CORPUS_FILE}.lock`, () => {
|
|
114
|
+
load(true);
|
|
115
|
+
_events.push(event);
|
|
116
|
+
if (_events.length > MAX_EVENTS) _events = _events.slice(-MAX_EVENTS);
|
|
117
|
+
_corpusDirty = true;
|
|
118
|
+
persist();
|
|
119
|
+
if (conflicts.length > 0) {
|
|
120
|
+
logger.warn({ id: event.id, tag: event.incidentTag, conflicts }, "override-corpus: conflict detected with existing entries");
|
|
121
|
+
}
|
|
122
|
+
logger.info({ id: event.id, tag: event.incidentTag, reason: event.overrideReason, service: event.service, expiresAt }, "override-corpus: override recorded");
|
|
123
|
+
return { ...event, conflictsWith: conflicts.length > 0 ? conflicts : void 0 };
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function getExpiringSoon(windowDays = 14) {
|
|
127
|
+
loadForRead();
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
const cutoffMs = now + windowDays * 24 * 60 * 60 * 1e3;
|
|
130
|
+
return _events.filter((e) => {
|
|
131
|
+
if (!e.expiresAt || e.expiresAt === 0) return false;
|
|
132
|
+
return e.expiresAt > now && e.expiresAt < cutoffMs;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
function overridePackKey(e) {
|
|
136
|
+
return `${e.incidentTag}:${e.overrideReason}:${e.dayOfWeek}:${e.hourOfDay}:${e.service}`;
|
|
137
|
+
}
|
|
138
|
+
function buildOverridePack(events, opts = {}) {
|
|
139
|
+
const seen = /* @__PURE__ */ new Set();
|
|
140
|
+
const entries = [];
|
|
141
|
+
for (const e of events) {
|
|
142
|
+
if (e.source === "community" && !opts.includeCommunity) continue;
|
|
143
|
+
const key = overridePackKey(e);
|
|
144
|
+
if (seen.has(key)) continue;
|
|
145
|
+
seen.add(key);
|
|
146
|
+
entries.push({
|
|
147
|
+
incidentTag: e.incidentTag,
|
|
148
|
+
proposedCommand: e.proposedCommand,
|
|
149
|
+
overrideReason: e.overrideReason,
|
|
150
|
+
...e.note ? { note: e.note } : {},
|
|
151
|
+
...e.rationale ? { rationale: e.rationale } : {},
|
|
152
|
+
service: e.service,
|
|
153
|
+
environment: e.environment,
|
|
154
|
+
dayOfWeek: e.dayOfWeek,
|
|
155
|
+
hourOfDay: e.hourOfDay,
|
|
156
|
+
...e.manualAction ? { manualAction: e.manualAction } : {},
|
|
157
|
+
...e.outcome ? { outcome: e.outcome } : {}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
format: "mergen-pack",
|
|
162
|
+
version: 1,
|
|
163
|
+
...opts.name ? { name: opts.name } : {},
|
|
164
|
+
exportedAt: Date.now(),
|
|
165
|
+
entryCount: entries.length,
|
|
166
|
+
entries
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function importOverrides(entries, opts = {}) {
|
|
170
|
+
const source = opts.source ?? "community";
|
|
171
|
+
const actor = opts.actor ?? source;
|
|
172
|
+
return lockAndExecute(`${OVERRIDE_CORPUS_FILE}.lock`, () => {
|
|
173
|
+
load(true);
|
|
174
|
+
const existingKeys = new Set(_events.map(overridePackKey));
|
|
175
|
+
const now = Date.now();
|
|
176
|
+
let imported = 0;
|
|
177
|
+
let skipped = 0;
|
|
178
|
+
for (const entry of entries) {
|
|
179
|
+
const dayOfWeek = Math.min(6, Math.max(0, Math.trunc(entry.dayOfWeek)));
|
|
180
|
+
const hourOfDay = Math.min(23, Math.max(0, Math.trunc(entry.hourOfDay)));
|
|
181
|
+
const key = overridePackKey({ ...entry, dayOfWeek, hourOfDay });
|
|
182
|
+
if (existingKeys.has(key)) {
|
|
183
|
+
skipped++;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
existingKeys.add(key);
|
|
187
|
+
_events.push({
|
|
188
|
+
id: randomUUID(),
|
|
189
|
+
incidentTag: entry.incidentTag,
|
|
190
|
+
proposedCommand: entry.proposedCommand.slice(0, 500),
|
|
191
|
+
overrideReason: entry.overrideReason,
|
|
192
|
+
note: entry.note?.slice(0, 200),
|
|
193
|
+
rationale: entry.rationale?.slice(0, 300),
|
|
194
|
+
service: entry.service,
|
|
195
|
+
environment: entry.environment,
|
|
196
|
+
dayOfWeek,
|
|
197
|
+
hourOfDay,
|
|
198
|
+
manualAction: entry.manualAction?.slice(0, 500),
|
|
199
|
+
outcome: entry.outcome,
|
|
200
|
+
recordedAt: now,
|
|
201
|
+
actor,
|
|
202
|
+
source,
|
|
203
|
+
expiresAt: entry.expiresInDays ? now + entry.expiresInDays * 24 * 60 * 60 * 1e3 : 0
|
|
204
|
+
});
|
|
205
|
+
imported++;
|
|
206
|
+
}
|
|
207
|
+
if (imported > 0) {
|
|
208
|
+
if (_events.length > MAX_EVENTS) _events = _events.slice(-MAX_EVENTS);
|
|
209
|
+
_corpusDirty = true;
|
|
210
|
+
persist();
|
|
211
|
+
logger.info({ imported, skipped, source }, "override-corpus: pack imported");
|
|
212
|
+
}
|
|
213
|
+
return { imported, skipped };
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
function updateOutcome(id, outcome) {
|
|
217
|
+
return lockAndExecute(`${OVERRIDE_CORPUS_FILE}.lock`, () => {
|
|
218
|
+
load(true);
|
|
219
|
+
const ev = _events.find((e) => e.id === id);
|
|
220
|
+
if (!ev) return false;
|
|
221
|
+
ev.outcome = outcome;
|
|
222
|
+
_corpusDirty = true;
|
|
223
|
+
persist();
|
|
224
|
+
return true;
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
function hasRecentOverride(incidentTag, service, dayOfWeek, hourOfDay) {
|
|
228
|
+
loadForRead();
|
|
229
|
+
const now = Date.now();
|
|
230
|
+
const cutoff = now - 90 * 24 * 60 * 60 * 1e3;
|
|
231
|
+
return _events.some((e) => {
|
|
232
|
+
if (e.incidentTag !== incidentTag) return false;
|
|
233
|
+
if (e.service !== service) return false;
|
|
234
|
+
if (e.recordedAt < cutoff) return false;
|
|
235
|
+
if (e.expiresAt && e.expiresAt > 0 && e.expiresAt < now) return false;
|
|
236
|
+
if (e.dayOfWeek !== dayOfWeek) return false;
|
|
237
|
+
const hourDiff = Math.abs(e.hourOfDay - hourOfDay);
|
|
238
|
+
return hourDiff <= 1 || hourDiff >= 23;
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
function dominantOverrideReason(incidentTag, service) {
|
|
242
|
+
loadForRead();
|
|
243
|
+
const counts = /* @__PURE__ */ new Map();
|
|
244
|
+
for (const e of _events) {
|
|
245
|
+
if (e.incidentTag !== incidentTag || e.service !== service) continue;
|
|
246
|
+
counts.set(e.overrideReason, (counts.get(e.overrideReason) ?? 0) + 1);
|
|
247
|
+
}
|
|
248
|
+
if (counts.size === 0) return null;
|
|
249
|
+
let best = null;
|
|
250
|
+
let bestCount = 0;
|
|
251
|
+
for (const [reason, count] of counts) {
|
|
252
|
+
if (count > bestCount) {
|
|
253
|
+
best = reason;
|
|
254
|
+
bestCount = count;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return best;
|
|
258
|
+
}
|
|
259
|
+
function getOverridesForTag(tag) {
|
|
260
|
+
loadForRead();
|
|
261
|
+
return _events.filter((e) => e.incidentTag === tag);
|
|
262
|
+
}
|
|
263
|
+
function getOverrideById(id) {
|
|
264
|
+
loadForRead();
|
|
265
|
+
return _events.find((e) => e.id === id) ?? null;
|
|
266
|
+
}
|
|
267
|
+
function getAllOverrides() {
|
|
268
|
+
loadForRead();
|
|
269
|
+
return _events;
|
|
270
|
+
}
|
|
271
|
+
const DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
272
|
+
const SIGNATURE_TOKEN_COUNT = 3;
|
|
273
|
+
const MAX_COMMAND_SIGNATURES = 5;
|
|
274
|
+
function commandSignature(proposedCommand) {
|
|
275
|
+
const normalized = normalizeForMatching(proposedCommand).trim();
|
|
276
|
+
return normalized.split(" ").slice(0, SIGNATURE_TOKEN_COUNT).join(" ");
|
|
277
|
+
}
|
|
278
|
+
function dominantCommandSignatures(events) {
|
|
279
|
+
const sigCounts = /* @__PURE__ */ new Map();
|
|
280
|
+
for (const e of events) {
|
|
281
|
+
const sig = commandSignature(e.proposedCommand);
|
|
282
|
+
if (sig) sigCounts.set(sig, (sigCounts.get(sig) ?? 0) + 1);
|
|
283
|
+
}
|
|
284
|
+
return [...sigCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, MAX_COMMAND_SIGNATURES).map(([sig]) => sig);
|
|
285
|
+
}
|
|
286
|
+
function compactCorpus() {
|
|
287
|
+
load();
|
|
288
|
+
if (!_corpusDirty && _compactedRules !== null) return _compactedRules;
|
|
289
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
290
|
+
for (const e of _events) {
|
|
291
|
+
const key = `${e.incidentTag}\0${e.service}\0${e.overrideReason}`;
|
|
292
|
+
const list = buckets.get(key) ?? [];
|
|
293
|
+
list.push(e);
|
|
294
|
+
buckets.set(key, list);
|
|
295
|
+
}
|
|
296
|
+
const rules = [];
|
|
297
|
+
for (const events of buckets.values()) {
|
|
298
|
+
if (events.length === 0) continue;
|
|
299
|
+
const first = events[0];
|
|
300
|
+
const dayCounts = /* @__PURE__ */ new Map();
|
|
301
|
+
for (const e of events) dayCounts.set(e.dayOfWeek, (dayCounts.get(e.dayOfWeek) ?? 0) + 1);
|
|
302
|
+
let dominantDay = null;
|
|
303
|
+
let dominantDayCount = 0;
|
|
304
|
+
for (const [day, count] of dayCounts) {
|
|
305
|
+
if (count > dominantDayCount) {
|
|
306
|
+
dominantDay = day;
|
|
307
|
+
dominantDayCount = count;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (dominantDay !== null && dominantDayCount / events.length < 0.4) dominantDay = null;
|
|
311
|
+
let hourWindow = null;
|
|
312
|
+
const hoursToWindow = dominantDay !== null ? events.filter((e) => e.dayOfWeek === dominantDay).map((e) => e.hourOfDay) : events.map((e) => e.hourOfDay);
|
|
313
|
+
if (hoursToWindow.length > 0) {
|
|
314
|
+
const minH = Math.min(...hoursToWindow);
|
|
315
|
+
const maxH = Math.max(...hoursToWindow);
|
|
316
|
+
if (maxH - minH < 20) hourWindow = [minH, maxH + 1];
|
|
317
|
+
}
|
|
318
|
+
const commandSignatures = dominantCommandSignatures(events);
|
|
319
|
+
rules.push({
|
|
320
|
+
incidentTag: first.incidentTag,
|
|
321
|
+
service: first.service,
|
|
322
|
+
overrideReason: first.overrideReason,
|
|
323
|
+
dayOfWeek: dominantDay,
|
|
324
|
+
hourWindow,
|
|
325
|
+
occurrences: events.length,
|
|
326
|
+
commandSignatures,
|
|
327
|
+
compactedAt: Date.now()
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
rules.sort((a, b) => b.occurrences - a.occurrences);
|
|
331
|
+
_compactedRules = rules;
|
|
332
|
+
_corpusDirty = false;
|
|
333
|
+
return rules;
|
|
334
|
+
}
|
|
335
|
+
function getRulesForTag(incidentTag, service) {
|
|
336
|
+
return compactCorpus().filter(
|
|
337
|
+
(r) => r.incidentTag === incidentTag && r.service === service
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
function describeTopRule(incidentTag, service) {
|
|
341
|
+
const rules = getRulesForTag(incidentTag, service);
|
|
342
|
+
if (rules.length === 0) return null;
|
|
343
|
+
const r = rules[0];
|
|
344
|
+
const timePart = r.dayOfWeek !== null ? ` (${DAY_NAMES[r.dayOfWeek]}${r.hourWindow ? ` ${r.hourWindow[0]}\u2013${r.hourWindow[1] - 1} UTC` : ""}, ${r.occurrences} overrides)` : ` (${r.occurrences} overrides)`;
|
|
345
|
+
return `${r.incidentTag} for ${r.service} \u2014 ${r.overrideReason}${timePart}`;
|
|
346
|
+
}
|
|
347
|
+
function getOverrideSummary() {
|
|
348
|
+
loadForRead();
|
|
349
|
+
const byTag = /* @__PURE__ */ new Map();
|
|
350
|
+
for (const e of _events) {
|
|
351
|
+
const list = byTag.get(e.incidentTag) ?? [];
|
|
352
|
+
list.push(e);
|
|
353
|
+
byTag.set(e.incidentTag, list);
|
|
354
|
+
}
|
|
355
|
+
const DAY_NAMES2 = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
356
|
+
const out = [];
|
|
357
|
+
for (const [tag, events] of byTag) {
|
|
358
|
+
const reasonCounts = /* @__PURE__ */ new Map();
|
|
359
|
+
const services = /* @__PURE__ */ new Set();
|
|
360
|
+
const dayCounts = /* @__PURE__ */ new Map();
|
|
361
|
+
const outcomes = { resolved: 0, escalated: 0, unresolved: 0, unknown: 0 };
|
|
362
|
+
for (const e of events) {
|
|
363
|
+
reasonCounts.set(e.overrideReason, (reasonCounts.get(e.overrideReason) ?? 0) + 1);
|
|
364
|
+
services.add(e.service);
|
|
365
|
+
dayCounts.set(e.dayOfWeek, (dayCounts.get(e.dayOfWeek) ?? 0) + 1);
|
|
366
|
+
if (e.outcome) outcomes[e.outcome]++;
|
|
367
|
+
else outcomes.unknown++;
|
|
368
|
+
}
|
|
369
|
+
let dominant = null;
|
|
370
|
+
let bestCount = 0;
|
|
371
|
+
for (const [r, c] of reasonCounts) {
|
|
372
|
+
if (c > bestCount) {
|
|
373
|
+
dominant = r;
|
|
374
|
+
bestCount = c;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
let dominantDay = null;
|
|
378
|
+
let dominantDayCount = 0;
|
|
379
|
+
for (const [day, count] of dayCounts) {
|
|
380
|
+
if (count > dominantDayCount) {
|
|
381
|
+
dominantDay = day;
|
|
382
|
+
dominantDayCount = count;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
let timePattern = null;
|
|
386
|
+
if (dominantDay !== null && dominantDayCount / events.length >= 0.4) {
|
|
387
|
+
const hoursOnDay = events.filter((e) => e.dayOfWeek === dominantDay).map((e) => e.hourOfDay);
|
|
388
|
+
const minHour = Math.min(...hoursOnDay);
|
|
389
|
+
const maxHour = Math.max(...hoursOnDay);
|
|
390
|
+
timePattern = maxHour > minHour ? `${DAY_NAMES2[dominantDay]} ${minHour}\u2013${maxHour} UTC` : `${DAY_NAMES2[dominantDay]} ${minHour} UTC`;
|
|
391
|
+
}
|
|
392
|
+
out.push({
|
|
393
|
+
tag,
|
|
394
|
+
total: events.length,
|
|
395
|
+
dominantReason: dominant,
|
|
396
|
+
services: [...services],
|
|
397
|
+
timePattern,
|
|
398
|
+
outcomes
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
out.sort((a, b) => b.total - a.total);
|
|
402
|
+
return out;
|
|
403
|
+
}
|
|
404
|
+
const _TAG_HINTS = [
|
|
405
|
+
[/\bpool\b|connection.?limit|max.?conn/i, "infra_db_connection_pool"],
|
|
406
|
+
[/\boom\b|out.of.memory|heap|memory.?leak/i, "infra_oom_kill"],
|
|
407
|
+
[/rate.?limit|throttl/i, "infra_rate_limit_cascade"],
|
|
408
|
+
[/\bauth\b|\btoken\b|session|oauth|\bjwt\b/i, "auth_token_not_persisted"],
|
|
409
|
+
[/\bcache\b|\bredis\b|memcach/i, "cache_invalidation"],
|
|
410
|
+
[/\bdeploy\b|rollout|release/i, "deploy_config_drift"],
|
|
411
|
+
[/\bcert\b|\btls\b|\bssl\b/i, "infra_certificate_expiry"],
|
|
412
|
+
[/\bdisk\b|storage|volume|\bspace\b/i, "infra_disk_pressure"],
|
|
413
|
+
[/migrat|schema|alter.?table/i, "db_migration_lock"],
|
|
414
|
+
[/friday|settlement|batch|weekend|end.of.day/i, "batch_window_conflict"],
|
|
415
|
+
[/\bscale\b|replica|pod.?count|instance/i, "infra_scaling"],
|
|
416
|
+
[/slow.?query|high.?latency|\bp99\b|\bp95\b/i, "infra_slow_query"]
|
|
417
|
+
];
|
|
418
|
+
const _REASON_HINTS = [
|
|
419
|
+
[/\b(friday|saturday|sunday|settlement|batch.window|end.of.day|after.hours|market.hours|trading.hours)\b/i, "batch-window"],
|
|
420
|
+
[/\b(cost|budget|billing|too.expensive|over.budget|cost.ceiling)\b/i, "cost-constraint"],
|
|
421
|
+
[/\b(cab|change.?control|change.?freeze|compliance|requires.approval|approval.required)\b/i, "compliance-hold"],
|
|
422
|
+
[/\b(read.?replica|replica.?routing|prefer.replica|read.only.replica)\b/i, "prefer-read-replica"],
|
|
423
|
+
[/\b(maintenance.?window|scheduled.downtime|downtime.window)\b/i, "maintenance-window"],
|
|
424
|
+
[/\b(wrong.?diag|misidentif|incorrect.root.cause|false.?positive|misdiagnos)\b/i, "wrong-diagnosis"],
|
|
425
|
+
[/\b(wrong.?fix|bad.command|revert|made.it.worse|incorrect.fix|undo)\b/i, "wrong-fix"]
|
|
426
|
+
];
|
|
427
|
+
function _inferTag(text) {
|
|
428
|
+
for (const [re, tag] of _TAG_HINTS) {
|
|
429
|
+
if (re.test(text)) return tag;
|
|
430
|
+
}
|
|
431
|
+
return "operational_constraint";
|
|
432
|
+
}
|
|
433
|
+
function _inferReason(text) {
|
|
434
|
+
for (const [re, reason] of _REASON_HINTS) {
|
|
435
|
+
if (re.test(text)) return reason;
|
|
436
|
+
}
|
|
437
|
+
return "on-call-discretion";
|
|
438
|
+
}
|
|
439
|
+
const _CLI_START_RE = /^(kubectl|oc|docker|docker-compose|podman|terraform|tofu|pulumi|ansible(?:-playbook)?|helm|kubeadm|nomad|vault|consul|systemctl|service|supervisorctl|pm2|npm|yarn|pnpm|npx|make|rake|rails|flyway|liquibase|alembic|psql|mysql|mongosh|mongo|redis-cli|aws|gcloud|az|git|pg_dump|pg_restore|createdb|dropdb|nginx|apachectl|kill|pkill|rm|chmod|chown|scp|rsync|bash|sh)\b/i;
|
|
440
|
+
const _ACTION_RE = /\b(restart|kill|delete|drop|destroy|scale|resize|run|migrate|execute|deploy|push|apply|rollback|terminate|truncate|alter|reboot|flush|drain|update|patch)\b/i;
|
|
441
|
+
const _NEG_RE = /\b(don'?t|do\s+not|avoid|never|should\s+not|shouldn'?t|must\s+not|mustn'?t|cannot|can'?t|won'?t|will\s+not|decided\s+(?:not|against)(?:\s+to)?|refrain\s+from|hold\s+off\s+on|no\s+(?:more\s+)?)\b/i;
|
|
442
|
+
const _CONSTRAINT_SIGNAL_RE = /\b(constraint:|policy:|rule:|adr:|decided:|going\s+forward[,:]?|lesson(?:\s+learned)?[s:]?|we\s+learned|action\s+item[s:]?|takeaway[s:]?|we\s+(?:shouldn'?t|should\s+not|decided\s+not|won'?t|will\s+not))\b/i;
|
|
443
|
+
const _TEMPORAL_RE = /\b(until|freeze|freeze\s+all|hold\s+off\s+until|not\s+until|embargo|blackout|lock\s+down|halt.*until|pause.*until|suspend.*until|stop.*until|don'?t.*until|this\s+(?:weekend|week|friday|monday|evening|night|morning)|end\s+of\s+(?:day|week|month|sprint|quarter))\b/i;
|
|
444
|
+
function _parseTemporalExpiry(sentence) {
|
|
445
|
+
const lc = sentence.toLowerCase();
|
|
446
|
+
if (/this\s+(weekend|saturday|sunday)/.test(lc)) {
|
|
447
|
+
const now = /* @__PURE__ */ new Date();
|
|
448
|
+
const daysUntilMon = (8 - now.getUTCDay()) % 7 || 7;
|
|
449
|
+
const expiry = new Date(now);
|
|
450
|
+
expiry.setUTCDate(now.getUTCDate() + daysUntilMon);
|
|
451
|
+
expiry.setUTCHours(6, 0, 0, 0);
|
|
452
|
+
return expiry.getTime();
|
|
453
|
+
}
|
|
454
|
+
if (/end\s+of\s+day|tonight|this\s+evening/.test(lc)) {
|
|
455
|
+
const expiry = /* @__PURE__ */ new Date();
|
|
456
|
+
expiry.setUTCHours(23, 59, 0, 0);
|
|
457
|
+
if (expiry.getTime() < Date.now()) expiry.setUTCDate(expiry.getUTCDate() + 1);
|
|
458
|
+
return expiry.getTime();
|
|
459
|
+
}
|
|
460
|
+
if (/this\s+week|end\s+of\s+week/.test(lc)) {
|
|
461
|
+
return Date.now() + 7 * 24 * 60 * 60 * 1e3;
|
|
462
|
+
}
|
|
463
|
+
if (/end\s+of\s+month/.test(lc)) {
|
|
464
|
+
return Date.now() + 30 * 24 * 60 * 60 * 1e3;
|
|
465
|
+
}
|
|
466
|
+
if (/end\s+of\s+(sprint|quarter)/.test(lc)) {
|
|
467
|
+
return Date.now() + 14 * 24 * 60 * 60 * 1e3;
|
|
468
|
+
}
|
|
469
|
+
return void 0;
|
|
470
|
+
}
|
|
471
|
+
function _extractCandidates(thread) {
|
|
472
|
+
const candidates = [];
|
|
473
|
+
const seen = /* @__PURE__ */ new Set();
|
|
474
|
+
function _add(command, note, isNegative, manualAction, expiresAt) {
|
|
475
|
+
const key = command.toLowerCase().slice(0, 50);
|
|
476
|
+
if (!seen.has(key)) {
|
|
477
|
+
seen.add(key);
|
|
478
|
+
candidates.push({ command: command.slice(0, 500), note: note.slice(0, 300), isNegative, manualAction, expiresAt });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const backtickCmds = [...thread.matchAll(/`([^`]{4,300})`/g)].map((m) => m[1].trim());
|
|
482
|
+
if (backtickCmds.length > 0) {
|
|
483
|
+
const proposed = backtickCmds[0];
|
|
484
|
+
const manual = backtickCmds[1];
|
|
485
|
+
_add(proposed, proposed, false, manual);
|
|
486
|
+
if (manual) seen.add(manual.toLowerCase().slice(0, 50));
|
|
487
|
+
for (const cmd of backtickCmds.slice(2)) _add(cmd, cmd, false);
|
|
488
|
+
}
|
|
489
|
+
for (const line of thread.split("\n")) {
|
|
490
|
+
const t = line.trim();
|
|
491
|
+
if (_CLI_START_RE.test(t)) _add(t, t, false);
|
|
492
|
+
}
|
|
493
|
+
for (const sentence of thread.split(/[.!?\n]+/).map((s) => s.trim()).filter((s) => s.length > 15 && s.length < 350)) {
|
|
494
|
+
if (!_NEG_RE.test(sentence)) continue;
|
|
495
|
+
if (!_ACTION_RE.test(sentence)) continue;
|
|
496
|
+
const lc = sentence.toLowerCase();
|
|
497
|
+
if ([...seen].some((k) => k.length > 10 && lc.includes(k))) continue;
|
|
498
|
+
const actionMatch = sentence.match(_ACTION_RE);
|
|
499
|
+
const verb = actionMatch ? actionMatch[0] : "operation";
|
|
500
|
+
_add(`${verb} (blocked): ${sentence.slice(0, 120)}`, sentence, true);
|
|
501
|
+
}
|
|
502
|
+
for (const sentence of thread.split(/[.!?\n]+/).map((s) => s.trim()).filter((s) => s.length > 10 && s.length < 400)) {
|
|
503
|
+
if (!_CONSTRAINT_SIGNAL_RE.test(sentence)) continue;
|
|
504
|
+
const lc = sentence.toLowerCase();
|
|
505
|
+
if ([...seen].some((k) => k.length > 10 && lc.includes(k))) continue;
|
|
506
|
+
_add(sentence.slice(0, 200), sentence, false);
|
|
507
|
+
}
|
|
508
|
+
for (const sentence of thread.split(/[.!?\n]+/).map((s) => s.trim()).filter((s) => s.length > 10 && s.length < 400)) {
|
|
509
|
+
if (!_TEMPORAL_RE.test(sentence)) continue;
|
|
510
|
+
const lc = sentence.toLowerCase();
|
|
511
|
+
if ([...seen].some((k) => k.length > 10 && lc.includes(k))) continue;
|
|
512
|
+
const expiresAt = _parseTemporalExpiry(sentence);
|
|
513
|
+
const label = expiresAt ? `temporal-hold (expires ${new Date(expiresAt).toISOString().slice(0, 16)}Z): ${sentence.slice(0, 120)}` : `temporal-hold: ${sentence.slice(0, 120)}`;
|
|
514
|
+
_add(label, sentence, true, void 0, expiresAt);
|
|
515
|
+
}
|
|
516
|
+
return candidates;
|
|
517
|
+
}
|
|
518
|
+
function compileOverridesFromSlackThread(slackThread, service = "unknown") {
|
|
519
|
+
if (!slackThread) return [];
|
|
520
|
+
const candidates = _extractCandidates(slackThread);
|
|
521
|
+
if (candidates.length === 0) return [];
|
|
522
|
+
const threadTag = _inferTag(slackThread);
|
|
523
|
+
const threadReason = _inferReason(slackThread);
|
|
524
|
+
const events = [];
|
|
525
|
+
for (const cand of candidates) {
|
|
526
|
+
const localTag = _inferTag(cand.note) !== "operational_constraint" ? _inferTag(cand.note) : threadTag;
|
|
527
|
+
const localReason = _inferReason(cand.note) !== "on-call-discretion" ? _inferReason(cand.note) : threadReason;
|
|
528
|
+
try {
|
|
529
|
+
const event = recordOverride({
|
|
530
|
+
incidentTag: localTag,
|
|
531
|
+
proposedCommand: cand.command,
|
|
532
|
+
overrideReason: localReason,
|
|
533
|
+
note: cand.note,
|
|
534
|
+
rationale: cand.isNegative ? "Auto-extracted: negative decision pattern from postmortem thread" : "Auto-extracted: constraint declaration from postmortem thread",
|
|
535
|
+
service,
|
|
536
|
+
environment: "production",
|
|
537
|
+
manualAction: cand.manualAction,
|
|
538
|
+
actor: "slack-nlp-parser",
|
|
539
|
+
// Temporal constraints carry an auto-computed expiry
|
|
540
|
+
expiresAt: cand.expiresAt
|
|
541
|
+
});
|
|
542
|
+
events.push(event);
|
|
543
|
+
} catch {
|
|
544
|
+
}
|
|
545
|
+
if (events.length >= 10) break;
|
|
546
|
+
}
|
|
547
|
+
return events;
|
|
548
|
+
}
|
|
549
|
+
function getStaleOverrides(daysThreshold = 60) {
|
|
550
|
+
loadForRead();
|
|
551
|
+
const cutoff = Date.now() - daysThreshold * 24 * 60 * 60 * 1e3;
|
|
552
|
+
return _events.filter((e) => {
|
|
553
|
+
const lastReviewed = e.reviewedAt ?? e.recordedAt;
|
|
554
|
+
return lastReviewed < cutoff;
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
function markOverrideReviewed(id) {
|
|
558
|
+
return lockAndExecute(`${OVERRIDE_CORPUS_FILE}.lock`, () => {
|
|
559
|
+
load(true);
|
|
560
|
+
const ev = _events.find((e) => e.id === id);
|
|
561
|
+
if (!ev) return false;
|
|
562
|
+
ev.reviewedAt = Date.now();
|
|
563
|
+
_corpusDirty = true;
|
|
564
|
+
persist();
|
|
565
|
+
logger.info({ id }, "override-corpus: entry marked reviewed");
|
|
566
|
+
return true;
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
function compileOverrideFromSlackThread(slackThread, service = "unknown") {
|
|
570
|
+
return compileOverridesFromSlackThread(slackThread, service)[0] ?? null;
|
|
571
|
+
}
|
|
572
|
+
export {
|
|
573
|
+
MAX_COMMAND_SIGNATURES,
|
|
574
|
+
OVERRIDE_REASONS,
|
|
575
|
+
buildOverridePack,
|
|
576
|
+
commandSignature,
|
|
577
|
+
compactCorpus,
|
|
578
|
+
compileOverrideFromSlackThread,
|
|
579
|
+
compileOverridesFromSlackThread,
|
|
580
|
+
describeTopRule,
|
|
581
|
+
detectConflicts,
|
|
582
|
+
dominantCommandSignatures,
|
|
583
|
+
dominantOverrideReason,
|
|
584
|
+
getAllOverrides,
|
|
585
|
+
getExpiringSoon,
|
|
586
|
+
getOverrideById,
|
|
587
|
+
getOverrideSummary,
|
|
588
|
+
getOverridesForTag,
|
|
589
|
+
getRulesForTag,
|
|
590
|
+
getStaleOverrides,
|
|
591
|
+
hasRecentOverride,
|
|
592
|
+
importOverrides,
|
|
593
|
+
markOverrideReviewed,
|
|
594
|
+
overridePackKey,
|
|
595
|
+
recordOverride,
|
|
596
|
+
updateOutcome
|
|
597
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { calibrationClassifier } from "./calibration-classifier.js";
|
|
2
|
+
import { getStatsForTag } from "./calibration.js";
|
|
3
|
+
import { serviceGraph } from "../sensor/service-graph.js";
|
|
4
|
+
import { postmortemStore } from "./postmortem-store.js";
|
|
5
|
+
import { DEFAULT_EXECUTION_THRESHOLD } from "./threshold-optimizer.js";
|
|
6
|
+
import logger from "../sensor/logger.js";
|
|
7
|
+
const BLAST_RISK_THRESHOLD_DELTA = {
|
|
8
|
+
low: 0,
|
|
9
|
+
medium: 0.05,
|
|
10
|
+
high: 0.1
|
|
11
|
+
};
|
|
12
|
+
function planningGate(hyp, service, executionThreshold = DEFAULT_EXECUTION_THRESHOLD) {
|
|
13
|
+
const confidence = hyp.confidenceScore ?? 0;
|
|
14
|
+
const tagStats = getStatsForTag(hyp.tag);
|
|
15
|
+
const classifierScore = calibrationClassifier.predict(
|
|
16
|
+
confidence,
|
|
17
|
+
tagStats?.accuracy ?? 0.5,
|
|
18
|
+
tagStats?.verdicts ?? 0,
|
|
19
|
+
tagStats?.trusted ?? false
|
|
20
|
+
);
|
|
21
|
+
const blastRisk = serviceGraph.getBlastRisk(service);
|
|
22
|
+
const upstreamImpact = serviceGraph.getUpstreamImpact(service).length;
|
|
23
|
+
const serviceUnobserved = upstreamImpact === 0 && blastRisk === "low";
|
|
24
|
+
if (serviceUnobserved) {
|
|
25
|
+
logger.warn({ service }, "planning-gate: service has no observed spans \u2014 applying +10pp threshold as precaution");
|
|
26
|
+
}
|
|
27
|
+
const histSuccessRate = tagStats?.trusted ? tagStats.accuracy : null;
|
|
28
|
+
const recentPms = postmortemStore.getByTag(hyp.tag, 10).filter((pm) => pm.service === service).slice(0, 5);
|
|
29
|
+
const recentServiceSuccessRate = recentPms.length >= 3 ? recentPms.filter((pm) => pm.causallyCorrect).length / recentPms.length : null;
|
|
30
|
+
const recentServiceSamples = recentPms.length;
|
|
31
|
+
const blended = confidence * 0.6 + classifierScore * 0.4;
|
|
32
|
+
const adjustedConfidence = Math.round(blended * 1e3) / 1e3;
|
|
33
|
+
const unobservedDelta = serviceUnobserved ? 0.1 : 0;
|
|
34
|
+
const effectiveThreshold = executionThreshold + BLAST_RISK_THRESHOLD_DELTA[blastRisk] + unobservedDelta;
|
|
35
|
+
const signals = {
|
|
36
|
+
classifierScore: Math.round(classifierScore * 1e3) / 1e3,
|
|
37
|
+
upstreamImpact,
|
|
38
|
+
blastRisk,
|
|
39
|
+
histSuccessRate: histSuccessRate !== null ? Math.round(histSuccessRate * 1e3) / 1e3 : null,
|
|
40
|
+
recentServiceSuccessRate: recentServiceSuccessRate !== null ? Math.round(recentServiceSuccessRate * 1e3) / 1e3 : null,
|
|
41
|
+
recentServiceSamples
|
|
42
|
+
};
|
|
43
|
+
if (classifierScore < 0.5) {
|
|
44
|
+
return {
|
|
45
|
+
execute: false,
|
|
46
|
+
reason: `classifier gate: P(correct)=${(classifierScore * 100).toFixed(0)}% < 50% \u2014 hypothesis pattern does not match historical correct fixes`,
|
|
47
|
+
adjustedConfidence,
|
|
48
|
+
signals
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (adjustedConfidence < effectiveThreshold) {
|
|
52
|
+
return {
|
|
53
|
+
execute: false,
|
|
54
|
+
reason: `adjusted confidence ${(adjustedConfidence * 100).toFixed(0)}% < ${(effectiveThreshold * 100).toFixed(0)}% threshold (blast risk: ${blastRisk}, classifier: ${(classifierScore * 100).toFixed(0)}%)`,
|
|
55
|
+
adjustedConfidence,
|
|
56
|
+
signals
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (blastRisk === "high" && (histSuccessRate !== null && histSuccessRate < 0.6)) {
|
|
60
|
+
return {
|
|
61
|
+
execute: false,
|
|
62
|
+
reason: `high blast risk (${upstreamImpact} upstream services) + low historical success rate (${(histSuccessRate * 100).toFixed(0)}%) \u2014 requires manual approval`,
|
|
63
|
+
adjustedConfidence,
|
|
64
|
+
signals
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (recentServiceSuccessRate !== null && recentServiceSuccessRate < 0.3) {
|
|
68
|
+
return {
|
|
69
|
+
execute: false,
|
|
70
|
+
reason: `service-specific failure: last ${recentServiceSamples} (${hyp.tag}, ${service}) incidents resolved correctly only ${Math.round(recentServiceSuccessRate * 100)}% of the time \u2014 requires manual review`,
|
|
71
|
+
adjustedConfidence,
|
|
72
|
+
signals
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
execute: true,
|
|
77
|
+
reason: `planning gate approved: adjusted confidence ${(adjustedConfidence * 100).toFixed(0)}%, classifier ${(classifierScore * 100).toFixed(0)}%, blast risk ${blastRisk}${recentServiceSuccessRate !== null ? `, service success ${Math.round(recentServiceSuccessRate * 100)}%` : ""}`,
|
|
78
|
+
adjustedConfidence,
|
|
79
|
+
signals
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export {
|
|
83
|
+
planningGate
|
|
84
|
+
};
|