feat(snapshot): plausibilitaets-checks (relay-quorum, drop, min-events)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jörg Lohrer 2026-04-28 08:10:07 +02:00
parent ccd7daf14d
commit 7e38b73785
2 changed files with 95 additions and 0 deletions

View File

@ -0,0 +1,36 @@
export interface CheckInput {
relaysQueried: number
relaysResponded: number
eventCount: number
minEvents: number
lastKnownGoodCount: number | undefined
newDeletionsCount: number
allowShrink: boolean
}
export function runChecks(input: CheckInput): void {
const quorum = Math.ceil(input.relaysQueried * 0.6)
if (input.relaysResponded < quorum) {
throw new Error(
`Relay-Quorum nicht erreicht: ${input.relaysResponded}/${input.relaysQueried} ` +
`(brauche mindestens ${quorum})`,
)
}
if (input.eventCount < input.minEvents) {
throw new Error(
`Event-Count ${input.eventCount} unter min-events ${input.minEvents}`,
)
}
if (input.lastKnownGoodCount !== undefined && !input.allowShrink) {
const drop = input.lastKnownGoodCount - input.eventCount
const dropPct = drop / input.lastKnownGoodCount
if (dropPct > 0.2 && drop > input.newDeletionsCount) {
throw new Error(
`Event-Count-Drop ${drop} (${(dropPct * 100).toFixed(0)}%) gegenueber ` +
`last-known-good ${input.lastKnownGoodCount}, ` +
`nur ${input.newDeletionsCount} korrespondierende kind:5. ` +
`Override mit --allow-shrink falls bewusst.`,
)
}
}
}

View File

@ -0,0 +1,59 @@
import { assertEquals, assertThrows } from '@std/assert'
import { runChecks } from '../src/core/checks.ts'
Deno.test('runChecks: weniger als 60% relays geantwortet -> hard-fail', () => {
assertThrows(
() => runChecks({
relaysQueried: 5, relaysResponded: 2,
eventCount: 27, minEvents: 1, lastKnownGoodCount: undefined,
newDeletionsCount: 0, allowShrink: false,
}),
Error, 'Relay-Quorum',
)
})
Deno.test('runChecks: event-count unter min-events -> hard-fail', () => {
assertThrows(
() => runChecks({
relaysQueried: 5, relaysResponded: 5,
eventCount: 0, minEvents: 1, lastKnownGoodCount: undefined,
newDeletionsCount: 0, allowShrink: false,
}),
Error, 'min-events',
)
})
Deno.test('runChecks: drop > 20% ohne kind:5 -> hard-fail', () => {
assertThrows(
() => runChecks({
relaysQueried: 5, relaysResponded: 5,
eventCount: 20, minEvents: 1, lastKnownGoodCount: 27,
newDeletionsCount: 0, allowShrink: false,
}),
Error, 'Event-Count-Drop',
)
})
Deno.test('runChecks: drop > 20% mit korrespondierenden kind:5 -> ok', () => {
runChecks({
relaysQueried: 5, relaysResponded: 5,
eventCount: 20, minEvents: 1, lastKnownGoodCount: 27,
newDeletionsCount: 7, allowShrink: false,
})
})
Deno.test('runChecks: --allow-shrink umgeht drop-check', () => {
runChecks({
relaysQueried: 5, relaysResponded: 5,
eventCount: 1, minEvents: 1, lastKnownGoodCount: 27,
newDeletionsCount: 0, allowShrink: true,
})
})
Deno.test('runChecks: erstlauf ohne cache + min-events=1 -> ok', () => {
runChecks({
relaysQueried: 5, relaysResponded: 5,
eventCount: 1, minEvents: 1, lastKnownGoodCount: undefined,
newDeletionsCount: 0, allowShrink: false,
})
})