import { REGISTRY_HEALTH_SCHEMA_VERSION, REGISTRY_HEALTH_SCORE_VERSION, type RegistryHealth, type RegistryHealthGlobalMeans, type RegistryMonitorEntryState, type RegistryMonitorState, } from "./schema" const DAY_MS = 24 * 60 * 60 * 1000 const DEFAULT_GLOBAL_MEANS: RegistryHealthGlobalMeans = { availability7d: 0.85, availability30d: 0.85, indexSchema: 0.9, itemValidity: 0.9, dryRun: 0.9, } const PRIOR_WEIGHTS = { availability7d: 24, availability30d: 24, indexSchema: 12, itemValidity: 10, dryRun: 3, } as const type RateSignal = { successes: number observations: number } export type RegistryHealthSignals = { availability7d: RateSignal availability30d: RateSignal indexSchema: RateSignal itemValidity: RateSignal dryRun: RateSignal hygiene: { https: boolean contentTypeJson: boolean | null noDuplicateNames: boolean | null nameMatches: boolean | null } } function round(value: number, digits: number) { const factor = 10 ** digits return Math.round((value + Number.EPSILON) * factor) / factor } function isWithinWindow(timestamp: string, now: Date, days: number) { const age = now.getTime() - new Date(timestamp).getTime() return age >= 0 && age < days * DAY_MS } function isBucketWithinWindow(date: string, now: Date, days: number) { return isWithinWindow(`${date}T00:00:00.000Z`, now, days) } function sumDailySignal( state: RegistryMonitorEntryState, now: Date, days: number, successesKey: | "availabilitySuccesses" | "schemaSuccesses" | "itemSuccesses" | "dryRunSuccesses", observationsKey: | "availabilityObservations" | "schemaObservations" | "itemObservations" | "dryRunObservations" ) { return state.daily.reduce( (signal, bucket) => { if (!isBucketWithinWindow(bucket.date, now, days)) { return signal } signal.successes += bucket[successesKey] signal.observations += bucket[observationsKey] return signal }, { successes: 0, observations: 0 } ) } export function getRegistryHealthSignals( state: RegistryMonitorEntryState, registryUrl: string, now: Date ): RegistryHealthSignals { const availability7d = state.recentIndex.reduce( (signal, observation) => { if ( observation.outcome === "bot_challenge" || !isWithinWindow(observation.checkedAt, now, 7) ) { return signal } signal.observations += 1 signal.successes += observation.outcome === "reachable" ? 1 : 0 return signal }, { successes: 0, observations: 0 } ) return { availability7d, availability30d: sumDailySignal( state, now, 30, "availabilitySuccesses", "availabilityObservations" ), indexSchema: sumDailySignal( state, now, 30, "schemaSuccesses", "schemaObservations" ), itemValidity: sumDailySignal( state, now, 30, "itemSuccesses", "itemObservations" ), dryRun: sumDailySignal( state, now, 30, "dryRunSuccesses", "dryRunObservations" ), hygiene: { https: registryUrl.startsWith("https://"), contentTypeJson: state.latestHygiene.contentTypeJson, noDuplicateNames: state.latestHygiene.noDuplicateNames, nameMatches: state.latestHygiene.nameMatches, }, } } function getObservedMean( signals: RateSignal[], minimumObservations: number, fallback: number ) { const eligible = signals.filter( (signal) => signal.observations >= minimumObservations ) const observations = eligible.reduce( (total, signal) => total + signal.observations, 0 ) if (observations === 0) { return fallback } const successes = eligible.reduce( (total, signal) => total + signal.successes, 0 ) return successes / observations } export function calculateGlobalMeans( state: RegistryMonitorState, registryUrls: Record, now: Date ): RegistryHealthGlobalMeans { const signals = Object.entries(state.registries).map(([name, entry]) => getRegistryHealthSignals(entry, registryUrls[name] ?? "http://invalid", now) ) return { availability7d: getObservedMean( signals.map((signal) => signal.availability7d), 24, DEFAULT_GLOBAL_MEANS.availability7d ), availability30d: getObservedMean( signals.map((signal) => signal.availability30d), 24, DEFAULT_GLOBAL_MEANS.availability30d ), indexSchema: getObservedMean( signals.map((signal) => signal.indexSchema), 12, DEFAULT_GLOBAL_MEANS.indexSchema ), itemValidity: getObservedMean( signals.map((signal) => signal.itemValidity), 10, DEFAULT_GLOBAL_MEANS.itemValidity ), dryRun: getObservedMean( signals.map((signal) => signal.dryRun), 3, DEFAULT_GLOBAL_MEANS.dryRun ), } } function smooth(signal: RateSignal, globalMean: number, priorWeight: number) { return ( (signal.successes + globalMean * priorWeight) / (signal.observations + priorWeight) ) } function getHygienePoints(value: boolean | null) { if (value === null) { return 1.25 } return value ? 2.5 : 0 } function getNonChallengeObservations(state: RegistryMonitorEntryState) { return state.recentIndex.filter( (observation) => observation.outcome !== "bot_challenge" ) } function getUnavailableSince(state: RegistryMonitorEntryState) { return state.availabilityOutageSince ?? state.firstObservedAt } function deriveStatus( state: RegistryMonitorEntryState, signals: RegistryHealthSignals, now: Date ) { const observations = getNonChallengeObservations(state) const latest = observations.at(-1) const unavailableFor = now.getTime() - new Date(getUnavailableSince(state)).getTime() const unavailable = unavailableFor >= DAY_MS && (state.availabilityRecoveryRequired || latest?.outcome === "unreachable") if (unavailable) { if (latest?.outcome === "reachable") { return { status: "unavailable" as const, statusReason: { code: "recovery_pending" as const, message: "Waiting for a second successful recovery check", }, } } return { status: "unavailable" as const, statusReason: { code: "index_unavailable" as const, message: "No successful index check in the last 24 hours", }, } } const observationSpan = latest ? new Date(latest.checkedAt).getTime() - new Date(state.firstObservedAt).getTime() : 0 if (observations.length < 24 || observationSpan < DAY_MS) { return { status: "observing" as const, statusReason: { code: "collecting_baseline" as const, message: observations.length < 24 ? `Collecting baseline data (${observations.length} of 24 checks)` : "Collecting a full day of baseline data", }, } } const trailingFailures = state.consecutiveIndexFailures const latestSchemaInvalid = latest?.outcome === "reachable" && latest.schemaValid === false const itemDegraded = signals.itemValidity.observations >= 10 && signals.itemValidity.successes / signals.itemValidity.observations < 0.9 const recentDryRuns = state.recentDryRuns.slice(-2) const dryRunDegraded = recentDryRuns.length === 2 && recentDryRuns.every((observation) => !observation.success) if (trailingFailures >= 3) { return { status: "degraded" as const, statusReason: { code: "consecutive_index_failures" as const, message: `The registry index failed ${trailingFailures} consecutive checks`, }, } } if (latestSchemaInvalid) { return { status: "degraded" as const, statusReason: { code: "index_schema_invalid" as const, message: "The registry index failed schema validation", }, } } if (itemDegraded) { return { status: "degraded" as const, statusReason: { code: "item_validation_failures" as const, message: "Sampled registry items are failing validation", }, } } if (dryRunDegraded) { return { status: "degraded" as const, statusReason: { code: "dry_run_failures" as const, message: "The last two CLI dry-run checks failed", }, } } if (state.availabilityRecoveryRequired) { return { status: "degraded" as const, statusReason: { code: "recovery_pending" as const, message: "Waiting for a second successful recovery check", }, } } return { status: "healthy" as const, statusReason: { code: "healthy_thresholds" as const, message: "Recent checks are within healthy thresholds", }, } } export function calculateRegistryHealth({ state, registryUrl, globalMeans, now, }: { state: RegistryMonitorEntryState registryUrl: string globalMeans: RegistryHealthGlobalMeans now: Date }): RegistryHealth { const signals = getRegistryHealthSignals(state, registryUrl, now) const availability7d = smooth( signals.availability7d, globalMeans.availability7d, PRIOR_WEIGHTS.availability7d ) const availability30d = smooth( signals.availability30d, globalMeans.availability30d, PRIOR_WEIGHTS.availability30d ) const indexSchema = smooth( signals.indexSchema, globalMeans.indexSchema, PRIOR_WEIGHTS.indexSchema ) const itemValidity = smooth( signals.itemValidity, globalMeans.itemValidity, PRIOR_WEIGHTS.itemValidity ) const dryRun = smooth( signals.dryRun, globalMeans.dryRun, PRIOR_WEIGHTS.dryRun ) const breakdown = { reliability: round( 45 * (0.65 * availability7d + 0.35 * availability30d), 3 ), correctness: round(10 * indexSchema + 15 * itemValidity, 3), installability: round(20 * dryRun, 3), hygiene: round( (signals.hygiene.https ? 2.5 : 0) + getHygienePoints(signals.hygiene.contentTypeJson) + getHygienePoints(signals.hygiene.noDuplicateNames) + getHygienePoints(signals.hygiene.nameMatches), 3 ), } const score = round( breakdown.reliability + breakdown.correctness + breakdown.installability + breakdown.hygiene, 3 ) const { status, statusReason } = deriveStatus(state, signals, now) const unavailableFor = now.getTime() - new Date(getUnavailableSince(state)).getTime() const hidden = status === "unavailable" && unavailableFor >= 7 * DAY_MS const latestObservation = state.recentIndex.at(-1) const checkedAt = latestObservation?.checkedAt ?? state.firstObservedAt return { schemaVersion: REGISTRY_HEALTH_SCHEMA_VERSION, scoreVersion: REGISTRY_HEALTH_SCORE_VERSION, status, statusReason, score, breakdown, availability7d: round(availability7d, 6), availability30d: round(availability30d, 6), monitoringLimited: latestObservation?.outcome === "bot_challenge", firstObservedAt: state.firstObservedAt, checkedAt, lastSuccessfulCheck: state.lastSuccessfulCheck, hidden, } } export { DEFAULT_GLOBAL_MEANS, PRIOR_WEIGHTS }