feat(coach): carry discovered drivers in the no-tools snapshot

The FDR cross-metric driver pairs were reachable only through the
get_correlations tool, so a local/no-tools provider got the coincident
flag but zero discovered drivers and the cross-metric layer silently
degraded. The system prompt's fallback to a snapshot driver field
pointed at a field that did not exist on that path.

Attach a bounded top-3 slice of the same gated, ranked driver output the
tool serves to the no-tools snapshot floor, gated on the derived layer
and shed first under prompt-budget pressure. Local users now reach
parity on the flagship cross-metric capability.
This commit is contained in:
Marc-André Bombeck committed 2026-06-26 13:12:34 +02:00
1 parent 99a6163343
commit b0be36c55b
3 files changed
+183

No files matched your search

@@ -0,0 +1,94 @@
/**
* RECON1 (D5-5) — the no-tools snapshot driver block surfaces the SAME gated,
* ranked drivers the get_correlations tool serves, bounded to the top N, and
* attaches nothing when no driver survives the quality gates.
*/
import { describe, expect, it, vi, beforeEach } from "vitest";
const readCoachCorrelations = vi.fn();
vi.mock("@/lib/ai/coach/tools/correlations-read", () => ({
readCoachCorrelations: (userId: string) => readCoachCorrelations(userId),
}));
import {
buildCorrelationsSnapshotBlock,
SNAPSHOT_DRIVER_CAP,
} from "@/lib/ai/coach/correlations-snapshot";
import type { CoachCorrelationDriver } from "@/lib/ai/coach/tools/correlations-read";
function driver(
behaviour: string,
outcome: string,
r: number,
): CoachCorrelationDriver {
return {
behaviour,
outcome,
direction: r >= 0 ? "higher" : "lower",
lagDays: 1,
n: 42,
r,
note: `Higher ${behaviour} tends to go with ${outcome} — not a cause.`,
};
}
describe("buildCorrelationsSnapshotBlock (D5-5)", () => {
beforeEach(() => {
readCoachCorrelations.mockReset();
});
it("carries driver pairs when the engine surfaces them", async () => {
readCoachCorrelations.mockResolvedValue({
present: true,
drivers: [
driver("time in daylight", "sleep duration", 0.41),
driver("mood", "resting heart rate", -0.33),
],
pairsTested: 16,
windowDays: 180,
});
const block = await buildCorrelationsSnapshotBlock("user-1");
expect(block).not.toBeNull();
expect(block!.drivers).toHaveLength(2);
expect(block!.drivers[0].behaviour).toBe("time in daylight");
expect(block!.pairsTested).toBe(16);
expect(block!.windowDays).toBe(180);
});
it("bounds the block to the top N ranked drivers", async () => {
const many = Array.from({ length: SNAPSHOT_DRIVER_CAP + 4 }, (_, i) =>
driver(`behaviour ${i}`, "sleep duration", 0.5 - i * 0.02),
);
readCoachCorrelations.mockResolvedValue({
present: true,
drivers: many,
pairsTested: 30,
windowDays: 180,
});
const block = await buildCorrelationsSnapshotBlock("user-1");
expect(block!.drivers).toHaveLength(SNAPSHOT_DRIVER_CAP);
// Order preserved from the already-ranked tool output (highest effect first).
expect(block!.drivers[0].behaviour).toBe("behaviour 0");
});
it("attaches nothing when no driver survives the quality gates", async () => {
readCoachCorrelations.mockResolvedValue({
present: false,
reason: "no_significant_pattern",
});
expect(await buildCorrelationsSnapshotBlock("user-1")).toBeNull();
});
it("attaches nothing when present but the driver list is empty (coincident-only)", async () => {
// The reader can be present purely for the coincident flag with zero
// discovered drivers — the snapshot floor adds the driver layer only.
readCoachCorrelations.mockResolvedValue({
present: true,
drivers: [],
coincident: { fired: true, contributing: [], day: "2026-06-26" },
pairsTested: 12,
windowDays: 180,
});
expect(await buildCorrelationsSnapshotBlock("user-1")).toBeNull();
});
});
+62
View File
@@ -0,0 +1,62 @@
/**
* RECON1 (D5-5) — discovered-driver block for the no-tools Coach snapshot.
*
* The FDR-controlled cross-metric driver pairs were reachable ONLY via the
* `get_correlations` tool, so a local / no-tools provider (Ollama) got the
* coincident-deviation flag but ZERO discovered drivers — the differentiating
* cross-metric layer silently degraded, and system-prompt rule 14's fallback
* clause ("any correlation/driver field the SNAPSHOT carries") pointed at a
* field that did not exist on the no-tools path.
*
* This block closes that gap by attaching a BOUNDED slice of the SAME gated,
* ranked output the tool serves — `readCoachCorrelations` runs the identical
* discovery scan + quality gates (effect-size floor, family-tautology
* exclusion, sample-size shrinkage, confidence tiering), so a local user sees
* exactly the quality-filtered drivers a cloud user's tool would. We keep only
* the top few by ranked effect so the no-tools prompt stays compact.
*
* Server-only — `readCoachCorrelations` reads `@/lib/db`. Fail-soft: a null
* return (no surviving driver / read hiccup) attaches nothing.
*/
import {
readCoachCorrelations,
type CoachCorrelationDriver,
} from "./tools/correlations-read";
/** How many ranked drivers reach the no-tools snapshot floor. */
export const SNAPSHOT_DRIVER_CAP = 3;
/** One compact driver line for the snapshot — descriptive, never causal. */
export interface CorrelationsSnapshotBlock {
/** Top discovered drivers by ranked effect, capped + descriptive. */
drivers: CoachCorrelationDriver[];
/** How many behaviour×outcome pairs were tested (honest footer). */
pairsTested: number;
/** Trailing-day window the discovery scanned. */
windowDays: number;
}
/**
* Build the bounded discovered-driver block for the no-tools snapshot floor, or
* `null` when no driver survives the quality gates (or the read fails). The
* coincident flag is intentionally NOT duplicated here — the derived block
* already carries it; this block adds only the discovered-driver layer the
* no-tools path was missing.
*/
export async function buildCorrelationsSnapshotBlock(
userId: string,
): Promise<CorrelationsSnapshotBlock | null> {
const result = await readCoachCorrelations(userId);
if (!result.present || !result.drivers || result.drivers.length === 0) {
return null;
}
// `result.drivers` arrives already ranked by the discovery engine (shrunk
// effect magnitude, q as tie-break) and already gated (below-floor pairs
// dropped, tautologies excluded) — so the top-N slice is the highest-signal
// quality-filtered drivers, identical to what the tool path would surface.
return {
drivers: result.drivers.slice(0, SNAPSHOT_DRIVER_CAP),
pairsTested: result.pairsTested ?? 0,
windowDays: result.windowDays ?? 0,
};
}
+27
View File
@@ -46,6 +46,7 @@ import { annotate } from "@/lib/logging/context";
import { memoizePerRequest } from "@/lib/request-cache";
import { buildGlp1SnapshotBlock } from "./glp1-snapshot";
import { buildDerivedSnapshotBlock } from "./derived-snapshot";
import { buildCorrelationsSnapshotBlock } from "./correlations-snapshot";
import { buildCoachMemoryBlock } from "./memory-snapshot";
import { buildTrajectorySnapshotBlock } from "./trajectory-snapshot";
import { buildCycleSnapshotBlock } from "./cycle-snapshot";
@@ -1259,6 +1260,16 @@ async function buildCoachSnapshotImpl(
const trajectoryBlockPromise = derivedActive
? buildTrajectorySnapshotBlock(userId, derivedProfile, now)
: null;
// RECON1 (D5-5) — discovered cross-metric driver pairs for the no-tools
// snapshot floor. Reuses the SAME gated/ranked output the get_correlations
// tool serves (effect-size floor + family-tautology exclusion + shrinkage +
// confidence tiering already applied inside the discovery engine), so a
// local/no-tools provider reaches parity on the flagship cross-metric layer
// instead of getting only the coincident flag. Gated on `derivedActive` (it
// is the recovery / cross-metric layer) and fail-soft to null.
const correlationsBlockPromise = derivedActive
? buildCorrelationsSnapshotBlock(userId)
: null;
const memoryBlockPromise = buildCoachMemoryBlock(
userId,
derivedProfile,
@@ -1305,6 +1316,7 @@ async function buildCoachSnapshotImpl(
derivedBlock,
dayStrainRows,
trajectoryBlock,
correlationsBlock,
memoryBlock,
cycleBlock,
illnessBlock,
@@ -1320,6 +1332,7 @@ async function buildCoachSnapshotImpl(
derivedBlockPromise,
dayStrainRowsPromise,
trajectoryBlockPromise,
correlationsBlockPromise,
memoryBlockPromise,
cycleBlockPromise,
illnessBlockPromise,
@@ -2269,6 +2282,20 @@ async function buildCoachSnapshotImpl(
snapshot.trajectory = trajectoryBlock;
registerBlock("trajectory", "skin_temp");
}
// ── RECON1 (D5-5) — discovered cross-metric drivers ──────────────────
// The bounded top-N driver pairs (post quality-gate, post-rank) from the
// SAME engine the get_correlations tool reads, attached so the no-tools /
// local-provider path narrates the cross-metric layer it was missing —
// closing the parity gap with the tool path and making system-prompt rule
// 14's "any driver field the SNAPSHOT carries" fallback real. Descriptive,
// never causal. Registered against the lowest-priority `skin_temp`
// (environment) cluster so the budget degrader sheds it before any
// clinical block under prompt-budget pressure.
if (correlationsBlock) {
snapshot.correlations = correlationsBlock;
registerBlock("correlations", "skin_temp");
}
}
// ── v1.11.0 W5a — rolling-profile memory (Pillar P2 2a) ──────────────