-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpatterns.ts
More file actions
82 lines (72 loc) · 2.86 KB
/
Copy pathpatterns.ts
File metadata and controls
82 lines (72 loc) · 2.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
import type { Ledger, Gene } from './ledger'
export type Pattern = 'flapping' | 'seasonal-candidate' | 'decaying' | 'revived' | 'stable'
const DAY_MS = 24 * 60 * 60 * 1000
function invocationDates(gene: Gene): string[] {
return Object.keys(gene.invocations)
.filter(d => gene.invocations[d] > 0)
.sort()
}
export function weeksIdle(gene: Gene, todayISO: string): number {
const dates = invocationDates(gene)
const last = dates.length > 0 ? dates.at(-1)! : gene.born
return Math.floor((Date.parse(todayISO) - Date.parse(last)) / DAY_MS / 7)
}
export function isFlapping(gene: Gene, lookback = 4): boolean {
const toggles = gene.events.filter(e => e.type === 'muted' || e.type === 'unmuted')
if (toggles.length < lookback) return false
const recent = toggles.slice(-lookback)
for (let i = 1; i < recent.length; i++) {
if (recent[i].type === recent[i - 1].type) return false
}
return true
}
export function isRevived(gene: Gene, thresholdWeeks: number): boolean {
const lastMute = [...gene.events].reverse().find(e => e.type === 'muted')
if (!lastMute) return false
const after = invocationDates(gene).filter(d => Date.parse(d) > Date.parse(lastMute.at))
if (after.length === 0) return false
const gapWeeks = (Date.parse(after[0]) - Date.parse(lastMute.at)) / DAY_MS / 7
return gapWeeks >= thresholdWeeks
}
export function hasSeasonalGapPattern(gene: Gene, thresholdWeeks: number): boolean {
const dates = invocationDates(gene)
for (let i = 1; i < dates.length; i++) {
const gapWeeks = (Date.parse(dates[i]) - Date.parse(dates[i - 1])) / DAY_MS / 7
if (gapWeeks >= thresholdWeeks) return true
}
return false
}
export function classifyGene(gene: Gene, todayISO: string): Pattern {
if (isFlapping(gene)) return 'flapping'
if (isRevived(gene, gene.muteThresholdWeeks)) return 'revived'
const idle = weeksIdle(gene, todayISO)
if (idle >= gene.muteThresholdWeeks) {
if (hasSeasonalGapPattern(gene, gene.muteThresholdWeeks)) return 'seasonal-candidate'
return 'decaying'
}
return 'stable'
}
export type PruneResult = {
toMute: string[]
flagSeasonal: string[]
removalCandidates: string[]
}
export function pruneCandidates(ledger: Ledger, todayISO: string): PruneResult {
const toMute: string[] = []
const flagSeasonal: string[] = []
const removalCandidates: string[] = []
for (const [key, gene] of Object.entries(ledger.genes)) {
if (gene.core || gene.state === 'muted' || gene.seasonal) continue
const pattern = classifyGene(gene, todayISO)
if (pattern === 'seasonal-candidate') {
flagSeasonal.push(key)
continue
}
if (pattern === 'decaying') {
toMute.push(key)
const priorDecayMutes = gene.events.filter(e => e.type === 'muted' && e.reason === 'decay').length
if (priorDecayMutes >= 2) removalCandidates.push(key)
}
}
return { toMute, flagSeasonal, removalCandidates }
}