|
| 1 | +/** |
| 2 | + * Record-integrity contracts for perf metric records. |
| 3 | + * |
| 4 | + * A record that claims `pass === true` must actually carry the journey's |
| 5 | + * required measurements — a "passing" provision run with a null |
| 6 | + * `total_ms` is a lying record, not a pass. Failed records are exempt: |
| 7 | + * a run that errored mid-flight legitimately has nulls. |
| 8 | + */ |
| 9 | + |
| 10 | +import type { JourneySpec } from './journey' |
| 11 | + |
| 12 | +export interface IntegrityViolation { |
| 13 | + recordIndex: number |
| 14 | + journeyId: string |
| 15 | + field: string |
| 16 | + reason: 'null-required-field' | 'below-minimum' |
| 17 | + detail: string |
| 18 | +} |
| 19 | + |
| 20 | +export interface IntegrityResult { |
| 21 | + succeeded: boolean |
| 22 | + violations: IntegrityViolation[] |
| 23 | +} |
| 24 | + |
| 25 | +function isMissing(value: unknown): boolean { |
| 26 | + return value === null || value === undefined |
| 27 | +} |
| 28 | + |
| 29 | +/** |
| 30 | + * Validates flat metric records (Record<string, unknown> with a boolean |
| 31 | + * `pass` field) against their journey contract. Only records with |
| 32 | + * pass === true are checked — a failed record may legitimately have nulls. |
| 33 | + * resolveJourney maps a record to its JourneySpec (or null to skip). |
| 34 | + */ |
| 35 | +export function checkRecordIntegrity( |
| 36 | + records: ReadonlyArray<Record<string, unknown>>, |
| 37 | + resolveJourney: (record: Record<string, unknown>) => JourneySpec | null, |
| 38 | +): IntegrityResult { |
| 39 | + const violations: IntegrityViolation[] = [] |
| 40 | + for (const [recordIndex, record] of records.entries()) { |
| 41 | + if (record.pass !== true) continue |
| 42 | + const journey = resolveJourney(record) |
| 43 | + if (journey === null) continue |
| 44 | + for (const field of journey.requiredFields) { |
| 45 | + if (isMissing(record[field])) { |
| 46 | + violations.push({ |
| 47 | + recordIndex, |
| 48 | + journeyId: journey.id, |
| 49 | + field, |
| 50 | + reason: 'null-required-field', |
| 51 | + detail: `required field '${field}' is ${record[field] === null ? 'null' : 'undefined'} on a passing '${journey.id}' record`, |
| 52 | + }) |
| 53 | + } |
| 54 | + } |
| 55 | + for (const field of journey.phaseFields ?? []) { |
| 56 | + if (isMissing(record[field])) { |
| 57 | + violations.push({ |
| 58 | + recordIndex, |
| 59 | + journeyId: journey.id, |
| 60 | + field, |
| 61 | + reason: 'null-required-field', |
| 62 | + detail: `phase field '${field}' is ${record[field] === null ? 'null' : 'undefined'} on a passing '${journey.id}' record`, |
| 63 | + }) |
| 64 | + } |
| 65 | + } |
| 66 | + for (const { field, min } of journey.minimums ?? []) { |
| 67 | + const value = record[field] |
| 68 | + if (isMissing(value)) continue // null-ness is the required/phase fields' contract |
| 69 | + if (typeof value !== 'number' || Number.isNaN(value)) { |
| 70 | + violations.push({ |
| 71 | + recordIndex, |
| 72 | + journeyId: journey.id, |
| 73 | + field, |
| 74 | + reason: 'below-minimum', |
| 75 | + detail: `field '${field}' has non-numeric value ${JSON.stringify(value)} on a passing '${journey.id}' record (minimum ${min})`, |
| 76 | + }) |
| 77 | + continue |
| 78 | + } |
| 79 | + if (value < min) { |
| 80 | + violations.push({ |
| 81 | + recordIndex, |
| 82 | + journeyId: journey.id, |
| 83 | + field, |
| 84 | + reason: 'below-minimum', |
| 85 | + detail: `field '${field}' is ${value}, below minimum ${min} on a passing '${journey.id}' record`, |
| 86 | + }) |
| 87 | + } |
| 88 | + } |
| 89 | + } |
| 90 | + return { succeeded: violations.length === 0, violations } |
| 91 | +} |
| 92 | + |
| 93 | +/** Throws an Error listing every violation when the result fails. */ |
| 94 | +export function assertRecordIntegrity( |
| 95 | + records: ReadonlyArray<Record<string, unknown>>, |
| 96 | + resolveJourney: (record: Record<string, unknown>) => JourneySpec | null, |
| 97 | +): void { |
| 98 | + const result = checkRecordIntegrity(records, resolveJourney) |
| 99 | + if (result.succeeded) return |
| 100 | + const lines = result.violations.map( |
| 101 | + (v) => ` [record ${v.recordIndex}] ${v.journeyId}.${v.field} (${v.reason}): ${v.detail}`, |
| 102 | + ) |
| 103 | + throw new Error( |
| 104 | + `Record integrity check failed with ${result.violations.length} violation(s):\n${lines.join('\n')}`, |
| 105 | + ) |
| 106 | +} |
0 commit comments