islandflow/packages/storage/src/alerts.ts

128 lines
3.7 KiB
TypeScript

import type { AlertEvent, ClassifierHit, SmartMoneyProfileScore } from "@islandflow/types";
export const ALERTS_TABLE = "alerts";
export type AlertRecord = {
source_ts: number;
ingest_ts: number;
seq: number;
trace_id: string;
score: number;
severity: string;
hits_json: string;
evidence_refs_json: string;
primary_profile_id: string;
profile_scores_json: string;
};
export const alertsTableDDL = (): string => {
return `
CREATE TABLE IF NOT EXISTS ${ALERTS_TABLE} (
source_ts UInt64,
ingest_ts UInt64,
seq UInt64,
trace_id String,
score Float64,
severity String,
hits_json String,
evidence_refs_json String,
primary_profile_id String DEFAULT '',
profile_scores_json String DEFAULT '[]'
)
ENGINE = MergeTree
ORDER BY (source_ts, seq)
`;
};
export const alertsTableMigrations = (): string[] => [
`ALTER TABLE ${ALERTS_TABLE} ADD COLUMN IF NOT EXISTS primary_profile_id String DEFAULT ''`,
`ALTER TABLE ${ALERTS_TABLE} ADD COLUMN IF NOT EXISTS profile_scores_json String DEFAULT '[]'`
];
export const toAlertRecord = (alert: AlertEvent): AlertRecord => {
return {
source_ts: alert.source_ts,
ingest_ts: alert.ingest_ts,
seq: alert.seq,
trace_id: alert.trace_id,
score: alert.score,
severity: alert.severity,
hits_json: JSON.stringify(alert.hits),
evidence_refs_json: JSON.stringify(alert.evidence_refs),
primary_profile_id: alert.primary_profile_id ?? "",
profile_scores_json: JSON.stringify(alert.profile_scores ?? [])
};
};
const safeHitArray = (value: string): ClassifierHit[] => {
try {
const parsed = JSON.parse(value);
if (Array.isArray(parsed)) {
return parsed.map((entry) => {
const record = entry as Partial<ClassifierHit>;
return {
classifier_id: String(record.classifier_id ?? ""),
confidence: Number(record.confidence ?? 0),
direction: String(record.direction ?? ""),
explanations: Array.isArray(record.explanations)
? record.explanations.map((item) => String(item))
: []
};
});
}
} catch {
// ignore
}
return [];
};
const safeStringArray = (value: string): string[] => {
try {
const parsed = JSON.parse(value);
if (Array.isArray(parsed)) {
return parsed.map((entry) => String(entry));
}
} catch {
// ignore
}
return [];
};
const safeProfileScoreArray = (value: string): SmartMoneyProfileScore[] => {
try {
const parsed = JSON.parse(value);
if (Array.isArray(parsed)) {
return parsed.map((entry) => {
const record = entry as Partial<SmartMoneyProfileScore>;
return {
profile_id: String(record.profile_id ?? "") as SmartMoneyProfileScore["profile_id"],
probability: Number(record.probability ?? 0),
confidence_band: String(record.confidence_band ?? "low") as SmartMoneyProfileScore["confidence_band"],
direction: String(record.direction ?? "unknown") as SmartMoneyProfileScore["direction"],
reasons: Array.isArray(record.reasons) ? record.reasons.map((item) => String(item)) : []
};
});
}
} catch {
// ignore
}
return [];
};
export const fromAlertRecord = (record: AlertRecord): AlertEvent => {
return {
source_ts: record.source_ts,
ingest_ts: record.ingest_ts,
seq: record.seq,
trace_id: record.trace_id,
score: record.score,
severity: record.severity,
hits: safeHitArray(record.hits_json),
evidence_refs: safeStringArray(record.evidence_refs_json),
...(record.primary_profile_id ? { primary_profile_id: record.primary_profile_id as AlertEvent["primary_profile_id"] } : {}),
profile_scores: safeProfileScoreArray(record.profile_scores_json)
};
};