|
| 1 | +// Maintainer-recap CONFIG-DRIFT section (#8214, epic #8211 track A). |
| 2 | +// |
| 3 | +// Pure section builder over a plain source struct, mirroring maintainer-recap-calibration.ts exactly: drift |
| 4 | +// alerts are point-in-time, but the weekly recap is where a STANDING drift should be impossible to miss. The |
| 5 | +// section renders each drifting knob's direction, live vs dominating value, corpus sizes, and how long the |
| 6 | +// episode has stood — aggregate numbers + knob ids only, never corpus content (the same public-safe boundary |
| 7 | +// as every other recap section). |
| 8 | +// |
| 9 | +// Ships independently of the sentinel runtime, the same way the calibration section shipped ahead of the full |
| 10 | +// RecapReport (#2243's own header): this file only needs the per-knob {@link KnobDriftReport} projection plus |
| 11 | +// the episode's first-fingerprinted timestamp, so a caller wires it the moment the sentinel persists episodes. |
| 12 | +// Until then the flag-off arm renders the explicit disabled line — absence of data must be distinguishable |
| 13 | +// from absence of drift. |
| 14 | +import { PUBLIC_LOCAL_PATH_SCRUB_PATTERN } from "../signals/redaction"; |
| 15 | +import type { KnobDriftReport } from "./loosening-knobs"; |
| 16 | + |
| 17 | +/** One standing drift episode: the sentinel's current report for a live knob, plus when the sentinel first |
| 18 | + * fingerprinted the episode (its fingerprint timestamp — the "how long has this stood" anchor). */ |
| 19 | +export type DriftRecapKnob = { |
| 20 | + report: KnobDriftReport; |
| 21 | + /** ISO timestamp of the episode's first sentinel fingerprint. */ |
| 22 | + episodeSince: string; |
| 23 | +}; |
| 24 | + |
| 25 | +/** Projection of the sentinel's state used by the drift section. Structurally compatible with what the |
| 26 | + * sentinel evaluates per live knob ({@link KnobDriftReport} via evaluateKnobDrift, loosening-knobs.ts). */ |
| 27 | +export type DriftRecapSource = { |
| 28 | + /** Recap generation instant — episode ages are computed against this, never against a wall-clock read. */ |
| 29 | + generatedAt: string; |
| 30 | + /** False ⇒ the drift sentinel is not running; the section says so explicitly instead of looking clean. */ |
| 31 | + sentinelEnabled: boolean; |
| 32 | + /** Every live knob the sentinel currently reports as drifting. */ |
| 33 | + drifting: DriftRecapKnob[]; |
| 34 | + /** Count of evaluated live knobs with NO standing drift. */ |
| 35 | + cleanKnobs: number; |
| 36 | +}; |
| 37 | + |
| 38 | +/** One titled digest section: structured fields for consumers + ready-to-emit lines for the formatter — |
| 39 | + * the CalibrationRecapSection shape verbatim, with drift counts in place of reversal counts. */ |
| 40 | +export type DriftRecapSection = { |
| 41 | + title: string; |
| 42 | + drifting: number; |
| 43 | + clean: number; |
| 44 | + /** Plain-English status line (disabled / clean / drift-present). */ |
| 45 | + note: string; |
| 46 | + lines: string[]; |
| 47 | +}; |
| 48 | + |
| 49 | +/** Public-safe scrub for free text pulled into the section (defense in depth — knob/rule ids and ISO |
| 50 | + * timestamps are the only string inputs today). Mirrors maintainer-recap-calibration.ts. */ |
| 51 | +function sanitizeRecapText(value: string): string { |
| 52 | + return value.replace(PUBLIC_LOCAL_PATH_SCRUB_PATTERN, "<redacted-path>").slice(0, 240); |
| 53 | +} |
| 54 | + |
| 55 | +/** Whole days an episode has stood at `generatedAt`, floored; clock skew that puts the fingerprint in the |
| 56 | + * future (or an unparseable timestamp) reads as 0 rather than a negative/NaN age. */ |
| 57 | +function episodeStandingDays(episodeSince: string, generatedAt: string): number { |
| 58 | + const elapsedMs = Date.parse(generatedAt) - Date.parse(episodeSince); |
| 59 | + return Number.isFinite(elapsedMs) && elapsedMs > 0 ? Math.floor(elapsedMs / 86_400_000) : 0; |
| 60 | +} |
| 61 | + |
| 62 | +/** |
| 63 | + * Pure config-drift section over the sentinel projection, mirroring {@link buildCalibrationRecapSection}'s |
| 64 | + * arms exactly: |
| 65 | + * |
| 66 | + * - sentinel off ⇒ the explicit disabled line (never a clean-looking silence); |
| 67 | + * - no drifting knobs ⇒ one clean summary line over `cleanKnobs`; |
| 68 | + * - drifting knobs ⇒ one line per knob (direction, live vs dominating value, corpus sizes, standing days), |
| 69 | + * plus the clean-knob summary when the window is mixed. |
| 70 | + */ |
| 71 | +export function buildDriftRecapSection(source: DriftRecapSource): DriftRecapSection { |
| 72 | + const title = "Config drift"; |
| 73 | + const drifting = source.drifting.length; |
| 74 | + |
| 75 | + if (!source.sentinelEnabled) { |
| 76 | + const note = "drift sentinel disabled — no drift evaluation ran this window."; |
| 77 | + return { title, drifting: 0, clean: 0, note: sanitizeRecapText(note), lines: [sanitizeRecapText(note)] }; |
| 78 | + } |
| 79 | + |
| 80 | + if (drifting === 0) { |
| 81 | + const note = `Config drift clean: all ${source.cleanKnobs} evaluated knob(s) remain their best-supported live values.`; |
| 82 | + return { title, drifting, clean: source.cleanKnobs, note: sanitizeRecapText(note), lines: [sanitizeRecapText(note)] }; |
| 83 | + } |
| 84 | + |
| 85 | + const note = `config drift: ${drifting} live knob(s) are Pareto-dominated by another supported value; longest-standing episodes first below.`; |
| 86 | + const knobLines = [...source.drifting] |
| 87 | + .sort((left, right) => episodeStandingDays(right.episodeSince, source.generatedAt) - episodeStandingDays(left.episodeSince, source.generatedAt)) |
| 88 | + .map(({ report, episodeSince }) => { |
| 89 | + const days = episodeStandingDays(episodeSince, source.generatedAt); |
| 90 | + return `${report.knobId} (${report.ruleId}): live ${report.liveValue} vs dominating ${report.dominatingValue} (${report.direction}) — visible n=${report.visibleCases}, held-out n=${report.heldOutCases}; standing ${days} day(s).`; |
| 91 | + }); |
| 92 | + const lines = [note, ...knobLines]; |
| 93 | + if (source.cleanKnobs > 0) lines.push(`${source.cleanKnobs} other evaluated knob(s) are clean.`); |
| 94 | + |
| 95 | + return { |
| 96 | + title, |
| 97 | + drifting, |
| 98 | + clean: source.cleanKnobs, |
| 99 | + note: sanitizeRecapText(note), |
| 100 | + lines: lines.map(sanitizeRecapText), |
| 101 | + }; |
| 102 | +} |
0 commit comments