Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions src/review/auto-apply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,10 +276,14 @@ async function loadShadowOverrideRow(env: StorageEnv, project: string): Promise<
/** Write a recommended override to the SHADOW queue with a future validated_until (the soak deadline). MERGED
* over any existing shadow row so a partial write never erases a prior queued tunable. (#partial-overwrite-fix)
* Preserves any existing clear_at rather than silently nulling it via INSERT OR REPLACE (#stale-clear-at-fix). */
export async function writeShadowOverride(env: StorageEnv, project: string, o: TunableOverride, validatedUntilIso: string): Promise<void> {
export async function writeShadowOverride(env: StorageEnv, project: string, o: TunableOverride, validatedUntilIso: string, nowIso?: string): Promise<void> {
const existingRow = await loadShadowOverrideRow(env, project);
const merged = mergeOverride(existingRow ? rowToOverride(existingRow) : null, o);
const clearAt = existingRow?.clear_at ?? null;
// #10291: mirror writeLiveOverride exactly. Thread nowIso through the merge read AND the clear_at
// preservation so an already-lapsed clear_at is DROPPED rather than resurrected — the shadow side only
// ported the "preserve the column" half of the #stale-clear-at-fix, not the "drop it once expired" half,
// so a stale shadow tightening could be promoted to live after its own operator-set expiry had passed.
const merged = mergeOverride(existingRow ? rowToOverride(existingRow, nowIso) : null, o);
const clearAt = existingRow && !clearAtIsExpired(existingRow.clear_at, nowIso) ? existingRow.clear_at : null;
await storage(env)
.prepare(
"INSERT OR REPLACE INTO tunables_overrides_shadow (project, confidence_floor, scope_cap_files, scope_cap_lines, applied_at, validated_until, clear_at) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, ?, ?)",
Expand All @@ -289,10 +293,12 @@ export async function writeShadowOverride(env: StorageEnv, project: string, o: T
}

/** Load the pending shadow override for a project (null if none / DB error). */
export async function loadShadowOverride(env: StorageEnv, project: string): Promise<ShadowOverride | null> {
export async function loadShadowOverride(env: StorageEnv, project: string, nowIso?: string): Promise<ShadowOverride | null> {
const row = await loadShadowOverrideRow(env, project);
if (!row) return null;
const override = rowToOverride(row);
// #10291: thread nowIso (mirroring loadOverride) so a shadow row whose clear_at has already lapsed is read
// back as cleared, not still-active — rowToOverride applies the same clearAtIsExpired rule the live read uses.
const override = rowToOverride(row, nowIso);
return override ? { override, validatedUntil: row.validated_until } : null;
}

Expand Down
19 changes: 19 additions & 0 deletions test/unit/auto-apply.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,25 @@ describe("writeShadowOverride / loadShadowOverride / deleteShadowOverride", () =
await writeShadowOverride(env, "g", { confidenceFloor: 0.95 }, "2026-06-25T00:00:00Z");
expect(tables.shadow.get("g")?.clear_at).toBe("2099-01-01T00:00:00Z");
});
// #10291: the shadow pair only ported the "preserve the column" half of #stale-clear-at-fix, not the
// "drop it once expired" half — mirror the live-side "does NOT resurrect an ALREADY-EXPIRED override" test.
it("#10291: writeShadowOverride DROPS an already-expired clear_at (and does not resurrect the expired floor) when nowIso is passed", async () => {
const { env, tables } = fakeEnv();
tables.shadow.set("g", { confidence_floor: 0.8, scope_cap_files: null, scope_cap_lines: null, validated_until: "2026-06-19T00:00:00Z", clear_at: "2020-01-01T00:00:00Z" });
await writeShadowOverride(env, "g", { scopeCap: { files: 3, lines: 100 } }, "2026-06-25T00:00:00Z", "2026-06-20T00:00:00Z");
const row = tables.shadow.get("g");
expect(row?.clear_at).toBeNull(); // the lapsed clear_at is dropped, not carried forward
expect(row?.confidence_floor).toBeNull(); // the expired floor is not resurrected into the merge
expect(row?.scope_cap_files).toBe(3); // the new write still applies normally
});
it("#10291: loadShadowOverride reads an already-expired clear_at row as cleared when nowIso is after it", async () => {
const { env, tables } = fakeEnv();
// Only a confidence_floor gated by an expired clear_at: rowToOverride drops it → the override is empty → null.
tables.shadow.set("g", { confidence_floor: 0.8, scope_cap_files: null, scope_cap_lines: null, validated_until: "2026-06-19T00:00:00Z", clear_at: "2020-01-01T00:00:00Z" });
expect(await loadShadowOverride(env, "g", "2026-06-20T00:00:00Z")).toBeNull();
// Without nowIso (the pre-#10291 caller convention) the row is still read active — additive, non-breaking.
expect(await loadShadowOverride(env, "g")).not.toBeNull();
});
it("loadShadowOverride returns null when the row maps to an EMPTY override (rowToOverride → null arm)", async () => {
const { env, tables } = fakeEnv();
tables.shadow.set("g", { confidence_floor: null, scope_cap_files: null, scope_cap_lines: null, validated_until: "2026-06-25T00:00:00Z" });
Expand Down