Skip to content

Commit d39e4cf

Browse files
committed
feat(calibration): config-drift section in the maintainer recap (#8214)
1 parent 3494297 commit d39e4cf

4 files changed

Lines changed: 235 additions & 1 deletion

File tree

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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+
}

src/services/maintainer-recap.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import { PUBLIC_LOCAL_PATH_SCRUB_PATTERN, PUBLIC_UNSAFE_PATTERN } from "../signals/redaction";
1414
import { deliverRecapToDiscord, deliverRecapToSlack } from "./notify-discord";
1515
import type { GatePrecisionReport } from "./gate-precision";
16+
import type { DriftRecapSection } from "./maintainer-recap-drift";
1617
import type { OutcomeCalibration } from "./outcome-calibration";
1718
import type { MaintainerRecapCohortCounts, MaintainerRecapRepo, RecapReport } from "../types";
1819
import { nowIso } from "../utils/json";
@@ -161,7 +162,7 @@ function recapSectionLines(items: string[], fallback: string): string[] {
161162
* (Summary, Totals, Per-repo), mirroring formatWeeklyValueReportMarkdown at weekly-value-report.ts. PURE
162163
* string function — no delivery, no I/O. Every free-text value is routed through {@link redactRecapLine} so no
163164
* reward/trust/score/path term can leak into the digest even if the input report was hand-built. (#2240) */
164-
export function formatMaintainerRecap(report: RecapReport): string {
165+
export function formatMaintainerRecap(report: RecapReport, options: { configDrift?: DriftRecapSection } = {}): string {
165166
const { totals } = report;
166167
const rate = totals.gateFalsePositiveRate !== null ? `${Math.round(totals.gateFalsePositiveRate * 100)}%` : "n/a";
167168
const perRepoLines = report.repos.map(
@@ -188,6 +189,11 @@ export function formatMaintainerRecap(report: RecapReport): string {
188189
"",
189190
"## Per-repo",
190191
...recapSectionLines(perRepoLines, "_No repositories in this window._"),
192+
// #8214: optional config-drift section (maintainer-recap-drift.ts) — appended only when the caller has a
193+
// sentinel projection to render, so every existing digest stays byte-identical until the sentinel wires in.
194+
...(options.configDrift
195+
? ["", `## ${redactRecapLine(options.configDrift.title)}`, ...recapSectionLines(options.configDrift.lines, "_No drift lines for this window._")]
196+
: []),
191197
];
192198
return `${lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd()}\n`;
193199
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
buildDriftRecapSection,
4+
type DriftRecapKnob,
5+
type DriftRecapSource,
6+
} from "../../src/services/maintainer-recap-drift";
7+
import type { KnobDriftReport } from "../../src/services/loosening-knobs";
8+
9+
const GENERATED_AT = "2026-07-23T12:00:00.000Z";
10+
11+
function driftReport(overrides: Partial<KnobDriftReport> = {}): KnobDriftReport {
12+
const comparison = { verdict: "improved", baseline: { precision: 0.7 }, proposed: { precision: 0.9 } } as unknown as KnobDriftReport["visible"];
13+
return {
14+
knobId: "ai_consensus_defect.confidenceFloor",
15+
ruleId: "ai_consensus_defect",
16+
liveValue: 0.6,
17+
dominatingValue: 0.8,
18+
direction: "tighter",
19+
visibleCases: 40,
20+
heldOutCases: 12,
21+
visible: comparison,
22+
heldOut: comparison,
23+
...overrides,
24+
};
25+
}
26+
27+
function drifting(episodeSince: string, overrides: Partial<KnobDriftReport> = {}): DriftRecapKnob {
28+
return { report: driftReport(overrides), episodeSince };
29+
}
30+
31+
function source(overrides: Partial<DriftRecapSource> = {}): DriftRecapSource {
32+
return { generatedAt: GENERATED_AT, sentinelEnabled: true, drifting: [], cleanKnobs: 0, ...overrides };
33+
}
34+
35+
describe("buildDriftRecapSection (#8214)", () => {
36+
it("renders the explicit disabled line when the sentinel flag is off — absence of data, not absence of drift", () => {
37+
const section = buildDriftRecapSection(source({ sentinelEnabled: false, drifting: [drifting(GENERATED_AT)], cleanKnobs: 5 }));
38+
expect(section.title).toBe("Config drift");
39+
expect(section.drifting).toBe(0);
40+
expect(section.clean).toBe(0);
41+
expect(section.note).toMatch(/drift sentinel disabled/);
42+
expect(section.lines).toEqual([section.note]);
43+
});
44+
45+
it("renders one clean summary line when every evaluated knob matches its best-supported value", () => {
46+
const section = buildDriftRecapSection(source({ cleanKnobs: 6 }));
47+
expect(section.drifting).toBe(0);
48+
expect(section.clean).toBe(6);
49+
expect(section.note).toMatch(/Config drift clean: all 6 evaluated knob\(s\)/);
50+
expect(section.lines).toEqual([section.note]);
51+
});
52+
53+
it("renders each drifting knob with direction, live vs dominating value, corpus sizes, and standing days", () => {
54+
const section = buildDriftRecapSection(
55+
source({ drifting: [drifting("2026-07-11T12:00:00.000Z")] }),
56+
);
57+
expect(section.drifting).toBe(1);
58+
expect(section.note).toMatch(/config drift: 1 live knob\(s\)/);
59+
expect(section.lines[1]).toBe(
60+
"ai_consensus_defect.confidenceFloor (ai_consensus_defect): live 0.6 vs dominating 0.8 (tighter) — visible n=40, held-out n=12; standing 12 day(s).",
61+
);
62+
// All-drifting window: no clean-summary trailer.
63+
expect(section.lines).toHaveLength(2);
64+
});
65+
66+
it("orders a mixed window longest-standing first and appends the clean-knob summary", () => {
67+
const section = buildDriftRecapSection(
68+
source({
69+
drifting: [
70+
drifting("2026-07-22T12:00:00.000Z", { knobId: "young.knob", direction: "looser", liveValue: 0.9, dominatingValue: 0.5 }),
71+
drifting("2026-07-01T12:00:00.000Z", { knobId: "old.knob", direction: "shipped" }),
72+
],
73+
cleanKnobs: 4,
74+
}),
75+
);
76+
expect(section.drifting).toBe(2);
77+
expect(section.clean).toBe(4);
78+
expect(section.lines[1]).toContain("old.knob");
79+
expect(section.lines[1]).toContain("standing 22 day(s)");
80+
expect(section.lines[2]).toContain("young.knob");
81+
expect(section.lines[2]).toContain("(looser)");
82+
expect(section.lines[3]).toBe("4 other evaluated knob(s) are clean.");
83+
});
84+
85+
it("clamps a future or unparseable episode timestamp to 0 standing days instead of a negative/NaN age", () => {
86+
const section = buildDriftRecapSection(
87+
source({
88+
drifting: [drifting("2026-08-01T12:00:00.000Z", { knobId: "future.knob" }), drifting("not-a-timestamp", { knobId: "garbled.knob" })],
89+
}),
90+
);
91+
for (const line of section.lines.slice(1)) expect(line).toContain("standing 0 day(s)");
92+
});
93+
94+
it("INVARIANT: only knob ids and aggregate numbers reach the section — never corpus/diff content, and local paths are scrubbed", () => {
95+
// A hostile knob id smuggling an absolute path is scrubbed by the shared recap pattern; the diff-bearing
96+
// comparison objects on the report never surface in any emitted line.
97+
const hostile = drifting("2026-07-20T12:00:00.000Z", {
98+
knobId: "/Users/operator/secret/corpus.knob",
99+
visible: { verdict: "improved", corpusDiff: "diff --git a/leak b/leak" } as unknown as KnobDriftReport["visible"],
100+
});
101+
const section = buildDriftRecapSection(source({ drifting: [hostile], cleanKnobs: 1 }));
102+
const emitted = section.lines.join("\n");
103+
expect(emitted).toContain("<redacted-path>");
104+
expect(emitted).not.toContain("/Users/operator");
105+
expect(emitted).not.toMatch(/diff --git|corpusDiff|verdict/);
106+
});
107+
});

test/unit/maintainer-recap-format.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, expect, it } from "vitest";
22
import { formatMaintainerRecap } from "../../src/services/maintainer-recap";
3+
import { buildDriftRecapSection } from "../../src/services/maintainer-recap-drift";
34
import type { RecapReport } from "../../src/types";
45

56
const GEN = "2026-07-08T00:00:00.000Z";
@@ -32,6 +33,9 @@ describe("formatMaintainerRecap (#2240)", () => {
3233
expect(body).toContain("## Summary");
3334
expect(body).toContain("## Totals");
3435
expect(body).toContain("## Per-repo");
36+
// #8214: without a sentinel projection the drift section is entirely absent — the digest stays
37+
// byte-identical to the pre-drift shape, not a dangling empty header.
38+
expect(body).not.toContain("## Config drift");
3539
// Empty sections show a single fallback line instead of dangling under the header.
3640
expect(body).toContain("_No summary lines for this window._");
3741
expect(body).toContain("_No repositories in this window._");
@@ -43,6 +47,21 @@ describe("formatMaintainerRecap (#2240)", () => {
4347
expect(body).not.toMatch(/\n{3,}/);
4448
});
4549

50+
it("appends the #8214 config-drift section as bullet lines when the caller supplies a sentinel projection", () => {
51+
const configDrift = buildDriftRecapSection({
52+
generatedAt: GEN,
53+
sentinelEnabled: false,
54+
drifting: [],
55+
cleanKnobs: 0,
56+
});
57+
const body = formatMaintainerRecap(emptyReport(), { configDrift });
58+
expect(body).toContain("## Config drift");
59+
expect(body).toContain("- drift sentinel disabled — no drift evaluation ran this window.");
60+
// The appended section keeps the digest's formatting invariants.
61+
expect(body.endsWith("\n")).toBe(true);
62+
expect(body).not.toMatch(/\n{3,}/);
63+
});
64+
4665
it("renders per-repo rows, a percent rate, and redacts both regex arms (path + economic term)", () => {
4766
const report: RecapReport = {
4867
generatedAt: GEN,

0 commit comments

Comments
 (0)