FRE-605: Implement Phase 4 Change Tracking & Merge Logic
- Create ChangeTracker class with full version history support - Document change recording with metadata - Snapshot creation and restoration - Change acceptance/rejection workflow - Change diff generation between snapshots - Event-based change notifications - Implement MergeLogic with screenplay-specific rules - Server change application with conflict detection - Auto-resolution for non-overlapping edits - Scene-aware merge rules (same-scene vs different-scene) - Manual conflict resolution workflow - Merge validation - Write comprehensive unit tests - Change recording and tracking tests - Snapshot management tests - Conflict resolution tests - Screenplay-specific merge rule tests - Document implementation in analysis/fre605_change_tracking_implementation.md Architecture: ChangeTracker integrates with Yjs document updates. MergeLogic applies screenplay-specific rules for concurrent edits. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
122
src/lib/analytics/kpi-service.ts
Normal file
122
src/lib/analytics/kpi-service.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { eq, and, gte, lte, desc } from "drizzle-orm";
|
||||
import { kpiSnapshots } from "../../db/schema";
|
||||
import type { DrizzleDB } from "../../db/config/migrations";
|
||||
import type { NewKPISnapshot, KPISnapshot } from "../../db/schema";
|
||||
|
||||
export type KPIKey =
|
||||
| "mau"
|
||||
| "paying_users"
|
||||
| "mrr"
|
||||
| "conversion_rate"
|
||||
| "churn_rate"
|
||||
| "cac"
|
||||
| "ltv"
|
||||
| "nps"
|
||||
| "viral_coefficient";
|
||||
|
||||
export const KPI_THRESHOLDS: Record<KPIKey, { warning: number; critical: number; direction: "higher" | "lower" }> = {
|
||||
mau: { warning: 1000, critical: 500, direction: "higher" },
|
||||
paying_users: { warning: 100, critical: 50, direction: "higher" },
|
||||
mrr: { warning: 5000, critical: 2000, direction: "higher" },
|
||||
conversion_rate: { warning: 2, critical: 1, direction: "higher" },
|
||||
churn_rate: { warning: 5, critical: 3, direction: "lower" },
|
||||
cac: { warning: 12, critical: 15, direction: "lower" },
|
||||
ltv: { warning: 100, critical: 80, direction: "higher" },
|
||||
nps: { warning: 40, critical: 20, direction: "higher" },
|
||||
viral_coefficient: { warning: 0.3, critical: 0.1, direction: "higher" },
|
||||
};
|
||||
|
||||
export async function recordKPI(
|
||||
db: DrizzleDB,
|
||||
kpiKey: KPIKey,
|
||||
value: number,
|
||||
periodStart: Date,
|
||||
periodEnd: Date,
|
||||
metadata?: Record<string, unknown>
|
||||
): Promise<KPISnapshot> {
|
||||
const snapshot: NewKPISnapshot = {
|
||||
kpiKey,
|
||||
kpiValue: value,
|
||||
periodStart,
|
||||
periodEnd,
|
||||
metadata: metadata ? JSON.stringify(metadata) : null,
|
||||
};
|
||||
const result = await db.insert(kpiSnapshots).values(snapshot).returning();
|
||||
return result[0];
|
||||
}
|
||||
|
||||
export async function getLatestKPI(
|
||||
db: DrizzleDB,
|
||||
kpiKey: KPIKey
|
||||
): Promise<KPISnapshot | undefined> {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(kpiSnapshots)
|
||||
.where(eq(kpiSnapshots.kpiKey, kpiKey))
|
||||
.orderBy(desc(kpiSnapshots.createdAt))
|
||||
.limit(1);
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
export async function getKPIHistory(
|
||||
db: DrizzleDB,
|
||||
kpiKey: KPIKey,
|
||||
periodStart?: Date,
|
||||
periodEnd?: Date
|
||||
): Promise<KPISnapshot[]> {
|
||||
const conditions: import("drizzle-orm").SQL[] = [eq(kpiSnapshots.kpiKey, kpiKey)];
|
||||
|
||||
if (periodStart) {
|
||||
conditions.push(gte(kpiSnapshots.periodStart, periodStart));
|
||||
}
|
||||
if (periodEnd) {
|
||||
conditions.push(lte(kpiSnapshots.periodEnd, periodEnd));
|
||||
}
|
||||
|
||||
return await db
|
||||
.select()
|
||||
.from(kpiSnapshots)
|
||||
.where(and(...conditions))
|
||||
.orderBy(kpiSnapshots.periodStart);
|
||||
}
|
||||
|
||||
export async function getAllLatestKPIs(db: DrizzleDB): Promise<Record<KPIKey, KPISnapshot | undefined>> {
|
||||
const result: Record<KPIKey, KPISnapshot | undefined> = {} as Record<KPIKey, KPISnapshot | undefined>;
|
||||
const keys = Object.keys(KPI_THRESHOLDS) as KPIKey[];
|
||||
|
||||
for (const key of keys) {
|
||||
result[key] = await getLatestKPI(db, key);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function checkKPIAgainstThreshold(
|
||||
kpiKey: KPIKey,
|
||||
value: number
|
||||
): { breached: boolean; severity: "warning" | "critical" | null } {
|
||||
const thresholds = KPI_THRESHOLDS[kpiKey];
|
||||
if (!thresholds) return { breached: false, severity: null };
|
||||
|
||||
const { warning, critical, direction } = thresholds;
|
||||
const isHigher = direction === "higher";
|
||||
|
||||
if (isHigher) {
|
||||
if (value <= critical) return { breached: true, severity: "critical" };
|
||||
if (value <= warning) return { breached: true, severity: "warning" };
|
||||
} else {
|
||||
if (value >= critical) return { breached: true, severity: "critical" };
|
||||
if (value >= warning) return { breached: true, severity: "warning" };
|
||||
}
|
||||
|
||||
return { breached: false, severity: null };
|
||||
}
|
||||
|
||||
export function getKPIStatus(
|
||||
kpiKey: KPIKey,
|
||||
value: number
|
||||
): "healthy" | "warning" | "critical" {
|
||||
const { breached, severity } = checkKPIAgainstThreshold(kpiKey, value);
|
||||
if (!breached) return "healthy";
|
||||
return severity === "critical" ? "critical" : "warning";
|
||||
}
|
||||
Reference in New Issue
Block a user