repositories / will
will
owned by admin
src/shared/retention.ts
Raw/**
* Pure retention policy (spec: audit hot 90 days, chat hot 365 days,
* move exactly once into versioned zstd JSONL archives, retained forever).
*/
export interface RetentionRow {
id: string
ts: string
}
export interface RetentionPolicy {
auditHotDays: number
chatHotDays: number
}
export const DEFAULT_RETENTION: RetentionPolicy = {
auditHotDays: 90,
chatHotDays: 365,
}
export function cutoffIso(now: Date, hotDays: number): string {
return new Date(now.getTime() - hotDays * 86_400_000).toISOString()
}
/** Rows whose ts is strictly older than the cutoff must be archived. */
export function rowsToArchive(rows: RetentionRow[], cutoff: string): RetentionRow[] {
return rows.filter((r) => r.ts < cutoff)
}
/** Month partition key `YYYY-MM` of an ISO timestamp. */
export function monthOf(ts: string): string {
return ts.slice(0, 7)
}
/**
* Partition rows to archive into one group per month.
* Each group becomes one immutable segment (multiple segments per month allowed).
*/
export function groupByMonth(rows: RetentionRow[]): Map<string, RetentionRow[]> {
const groups = new Map<string, RetentionRow[]>()
for (const row of rows) {
const month = monthOf(row.ts)
const bucket = groups.get(month)
if (bucket) bucket.push(row)
else groups.set(month, [row])
}
return groups
}
export const ARCHIVE_FORMAT_VERSION = 1
export interface SegmentSidecar {
version: number
kind: 'audit' | 'chat'
month: string
segment: string
count: number
firstId: string
lastId: string
firstTs: string
lastTs: string
sha256: string
bytes: number
}
export interface ArchiveQuery {
kind: 'audit' | 'chat'
since?: string
until?: string
q?: string
limit: number
}
export interface ArchivedRecord {
id: string
ts: string
record: unknown
}
/** Sidecars overlapping a time range can be selected without decompression. */
export function segmentsOverlapping<T extends { firstTs: string; lastTs: string }>(
sidecars: T[],
query: { since?: string; until?: string },
): T[] {
return sidecars.filter((s) => {
if (query.since && s.lastTs < query.since) return false
if (query.until && s.firstTs > query.until) return false
return true
})
}