import { z } from "zod" import { registryNamespaceSchema } from "../registry-directory" export const REGISTRY_HEALTH_SCHEMA_VERSION = 1 as const export const REGISTRY_HEALTH_SCORE_VERSION = 1 as const export const REGISTRY_DRY_RUN_VERSION = 1 as const export const REGISTRY_DRY_RUN_FAILURE_MESSAGE_MAX_LENGTH = 1000 export const registryHealthStatusSchema = z.enum([ "healthy", "observing", "degraded", "unavailable", ]) export const registryHealthStatusReasonCodeSchema = z.enum([ "healthy_thresholds", "collecting_baseline", "index_unavailable", "recovery_pending", "consecutive_index_failures", "index_schema_invalid", "item_validation_failures", "dry_run_failures", ]) export const registryHealthBreakdownSchema = z .object({ reliability: z.number().min(0).max(45), correctness: z.number().min(0).max(25), installability: z.number().min(0).max(20), hygiene: z.number().min(0).max(10), }) .strict() export const registryHealthSchema = z .object({ schemaVersion: z.literal(REGISTRY_HEALTH_SCHEMA_VERSION), scoreVersion: z.literal(REGISTRY_HEALTH_SCORE_VERSION), status: registryHealthStatusSchema, statusReason: z .object({ code: registryHealthStatusReasonCodeSchema, message: z.string().min(1), }) .strict() .optional(), score: z.number().min(0).max(100), breakdown: registryHealthBreakdownSchema, availability7d: z.number().min(0).max(1), availability30d: z.number().min(0).max(1), monitoringLimited: z.boolean(), firstObservedAt: z.string().datetime().optional(), checkedAt: z.string().datetime(), lastSuccessfulCheck: z.string().datetime().optional(), hidden: z.boolean(), }) .strict() export const registryHealthGlobalMeansSchema = z .object({ availability7d: z.number().min(0).max(1), availability30d: z.number().min(0).max(1), indexSchema: z.number().min(0).max(1), itemValidity: z.number().min(0).max(1), dryRun: z.number().min(0).max(1), }) .strict() export const registryHealthSnapshotSchema = z .object({ schemaVersion: z.literal(REGISTRY_HEALTH_SCHEMA_VERSION), scoreVersion: z.literal(REGISTRY_HEALTH_SCORE_VERSION), generatedAt: z.string().datetime(), globalMeans: registryHealthGlobalMeansSchema, registries: z.record(registryNamespaceSchema, registryHealthSchema), }) .strict() export const registryIndexObservationSchema = z .object({ checkedAt: z.string().datetime(), outcome: z.enum(["reachable", "unreachable", "bot_challenge"]), status: z.number().int().min(100).max(599).optional(), failureCode: z.string().optional(), durationMs: z.number().int().nonnegative(), responseSize: z.number().int().nonnegative().optional(), redirectCount: z.number().int().nonnegative(), schemaValid: z.boolean().optional(), contentTypeJson: z.boolean().optional(), duplicateNames: z.boolean().optional(), nameMatches: z.boolean().optional(), itemCount: z.number().int().nonnegative().optional(), }) .strict() export const registryItemObservationSchema = z .object({ checkedAt: z.string().datetime(), item: z.string(), success: z.boolean(), failureCode: z.string().optional(), durationMs: z.number().int().nonnegative(), }) .strict() export const registryDryRunObservationSchema = z .object({ checkedAt: z.string().datetime(), item: z.string(), success: z.boolean(), failureCode: z.string().optional(), failureMessage: z .string() .max(REGISTRY_DRY_RUN_FAILURE_MESSAGE_MAX_LENGTH) .optional(), durationMs: z.number().int().nonnegative(), }) .strict() export const registryHealthDailyBucketSchema = z .object({ date: z.string().date(), availabilitySuccesses: z.number().int().nonnegative(), availabilityObservations: z.number().int().nonnegative(), challengeObservations: z.number().int().nonnegative(), schemaSuccesses: z.number().int().nonnegative(), schemaObservations: z.number().int().nonnegative(), itemSuccesses: z.number().int().nonnegative(), itemObservations: z.number().int().nonnegative(), dryRunSuccesses: z.number().int().nonnegative(), dryRunObservations: z.number().int().nonnegative(), }) .strict() const registryMonitorEntryStateInputSchema = z .object({ firstObservedAt: z.string().datetime(), lastSuccessfulCheck: z.string().datetime().optional(), status: registryHealthStatusSchema, consecutiveSuccessfulIndexes: z.number().int().nonnegative().optional(), consecutiveIndexFailures: z.number().int().nonnegative().optional(), availabilityOutageSince: z.string().datetime().optional(), availabilityRecoveryRequired: z.boolean().optional(), itemCursor: z.number().int().nonnegative(), itemNames: z.array(z.string()), recentIndex: z.array(registryIndexObservationSchema), recentDryRuns: z.array(registryDryRunObservationSchema), daily: z.array(registryHealthDailyBucketSchema), latestHygiene: z .object({ contentTypeJson: z.boolean().nullable(), noDuplicateNames: z.boolean().nullable(), nameMatches: z.boolean().nullable(), }) .strict(), }) .strict() function getLegacyAvailabilityState( entry: z.infer ) { const observations = entry.recentIndex.filter( (observation) => observation.outcome !== "bot_challenge" ) let cursor = observations.length - 1 let consecutiveSuccessfulIndexes = 0 while (cursor >= 0) { const observation = observations[cursor] if (observation.outcome !== "reachable" || !observation.schemaValid) { break } consecutiveSuccessfulIndexes += 1 cursor -= 1 } const failureEnd = cursor let precedingFailures = 0 while (cursor >= 0 && observations[cursor].outcome === "unreachable") { precedingFailures += 1 cursor -= 1 } const consecutiveIndexFailures = consecutiveSuccessfulIndexes === 0 ? precedingFailures : 0 const recoveryRequired = entry.status === "unavailable" || (entry.status === "degraded" && precedingFailures >= 3) const failureStart = precedingFailures > 0 ? observations[failureEnd - precedingFailures + 1] : undefined const outageStart = consecutiveSuccessfulIndexes > 0 ? failureStart?.checkedAt : (entry.lastSuccessfulCheck ?? failureStart?.checkedAt) const availabilityOutageSince = recoveryRequired || consecutiveIndexFailures > 0 ? (outageStart ?? entry.firstObservedAt) : undefined return { consecutiveSuccessfulIndexes, consecutiveIndexFailures, availabilityOutageSince, availabilityRecoveryRequired: recoveryRequired, } } export const registryMonitorEntryStateSchema = registryMonitorEntryStateInputSchema.transform((entry) => { if ( entry.consecutiveSuccessfulIndexes !== undefined && entry.consecutiveIndexFailures !== undefined && entry.availabilityRecoveryRequired !== undefined ) { return entry as typeof entry & { consecutiveSuccessfulIndexes: number consecutiveIndexFailures: number availabilityRecoveryRequired: boolean } } return { ...entry, ...getLegacyAvailabilityState(entry), } }) export const registryMonitorStateSchema = z .object({ schemaVersion: z.literal(REGISTRY_HEALTH_SCHEMA_VERSION), scoreVersion: z.literal(REGISTRY_HEALTH_SCORE_VERSION), dryRunVersion: z.number().int().nonnegative().optional(), updatedAt: z.string().datetime(), lastDailyRunAt: z.string().datetime().optional(), lastWeeklyRunAt: z.string().datetime().optional(), registries: z.record( registryNamespaceSchema, registryMonitorEntryStateSchema ), }) .strict() export const registryMonitorRunSchema = z .object({ schemaVersion: z.literal(REGISTRY_HEALTH_SCHEMA_VERSION), startedAt: z.string().datetime(), completedAt: z.string().datetime(), mode: z.enum(["auto", "hourly", "daily", "weekly", "all"]), totals: z .object({ registries: z.number().int().nonnegative(), reachable: z.number().int().nonnegative(), unavailable: z.number().int().nonnegative(), challenges: z.number().int().nonnegative(), itemChecks: z.number().int().nonnegative(), dryRuns: z.number().int().nonnegative(), }) .strict(), results: z.record( registryNamespaceSchema, z .object({ index: registryIndexObservationSchema.optional(), items: z.array(registryItemObservationSchema), dryRun: registryDryRunObservationSchema.optional(), }) .strict() ), diagnostics: z.array(z.string()), }) .strict() export const registryMonitorOutputSchema = z .object({ state: registryMonitorStateSchema, snapshot: registryHealthSnapshotSchema, run: registryMonitorRunSchema, }) .strict() export type RegistryHealth = z.infer export type RegistryHealthStatusReasonCode = z.infer< typeof registryHealthStatusReasonCodeSchema > export type RegistryHealthBreakdown = z.infer< typeof registryHealthBreakdownSchema > export type RegistryHealthGlobalMeans = z.infer< typeof registryHealthGlobalMeansSchema > export type RegistryHealthSnapshot = z.infer< typeof registryHealthSnapshotSchema > export type RegistryIndexObservation = z.infer< typeof registryIndexObservationSchema > export type RegistryItemObservation = z.infer< typeof registryItemObservationSchema > export type RegistryDryRunObservation = z.infer< typeof registryDryRunObservationSchema > export type RegistryHealthDailyBucket = z.infer< typeof registryHealthDailyBucketSchema > export type RegistryMonitorEntryState = z.infer< typeof registryMonitorEntryStateSchema > export type RegistryMonitorState = z.infer export type RegistryMonitorRun = z.infer export type RegistryMonitorOutput = z.infer