Remove the Health-Score explainer switch, which switched nothing

The admin panel offered a toggle for the Health-Score explainer and
described it as the ? next to the delta line. That ? had already been
taken off the insights surface a release earlier, and the caption that
replaced it went away entirely when the health score was replaced by
the reference composite. Since then an operator could turn the switch
off, get a saved confirmation, and change absolutely nothing.

The toggle, its wording in all six languages, its place in the flag
matrix the app and the iOS client both read, and its column on
app_settings are all gone. Nothing is left to promise a control that
does not exist.

A guard goes in with it: every remaining assistant sub-flag has to be
read by some surface, and the panel and the plumbing that carry all of
them by construction do not count as proof.
This commit is contained in:
Marc-André Bombeck committed 2026-07-31 08:06:07 +02:00
1 parent 8e6880b04c
commit 80c8674ee1
35 files changed
+169 -131

No files matched your search

-4
View File
@@ -6176,10 +6176,6 @@
"correlations": {
"title": "Korrelationen",
"description": "Erzähl-Kachel zur Korrelation auf der Insights-Übersicht."
},
"healthScoreExplainer": {
"title": "Health-Score-Erklärer",
"description": "Das ?-Symbol neben der Health-Score-Differenz. Die Zahl bleibt unabhängig davon sichtbar."
}
},
"modules": {
-4
View File
@@ -6176,10 +6176,6 @@
"correlations": {
"title": "Correlations",
"description": "Correlation tile narration on the Insights overview."
},
"healthScoreExplainer": {
"title": "Health-Score explainer",
"description": "The ? glyph next to the Health-Score delta line. The delta digit stays visible regardless."
}
},
"modules": {
-4
View File
@@ -6176,10 +6176,6 @@
"correlations": {
"title": "Correlaciones",
"description": "Narración de la tarjeta de correlación en la vista general de Insights."
},
"healthScoreExplainer": {
"title": "Explicador de Health-Score",
"description": "El icono ? junto a la línea de delta del Health-Score. El dígito del delta permanece visible siempre."
}
},
"modules": {
-4
View File
@@ -6176,10 +6176,6 @@
"correlations": {
"title": "Corrélations",
"description": "Narration de la tuile de corrélation sur la vue d'ensemble Insights."
},
"healthScoreExplainer": {
"title": "Explicateur de Health-Score",
"description": "L'icône ? près de la ligne de delta du Health-Score. Le chiffre du delta reste visible quoi qu'il arrive."
}
},
"modules": {
-4
View File
@@ -6176,10 +6176,6 @@
"correlations": {
"title": "Correlazioni",
"description": "Narrazione del tile di correlazione sulla panoramica Insights."
},
"healthScoreExplainer": {
"title": "Spiegatore Health-Score",
"description": "L'icona ? accanto alla riga del delta dell'Health-Score. La cifra del delta resta visibile in ogni caso."
}
},
"modules": {
-4
View File
@@ -6176,10 +6176,6 @@
"correlations": {
"title": "Korelacje",
"description": "Narracja kafelka korelacji w przeglądzie Insights."
},
"healthScoreExplainer": {
"title": "Objaśnienie Health-Score",
"description": "Ikona ? obok linii delty Health-Score. Cyfra delty pozostaje widoczna niezależnie."
}
},
"modules": {
@@ -0,0 +1,13 @@
-- Remove the Health-Score explainer operator switch.
--
-- The toggle gated a caption beside the Health-Score delta line. That
-- caption's component was deleted when the health score was replaced by
-- the reference composite, and the trailing "?" affordance the admin
-- description still named had already been retired a release earlier.
-- The switch has had nothing to switch since; an operator flipping it
-- changed nothing while being told they had.
--
-- Destructive by design: the setting is gone, so its stored value goes
-- with it.
ALTER TABLE "app_settings"
DROP COLUMN IF EXISTS "assistant_health_score_explainer_enabled";
+7 -8
View File
@@ -4364,10 +4364,10 @@ model AppSettings {
defaultUserTimezone String? @map("default_user_timezone") @db.VarChar(64)
// v1.4.31 — Assistant-surface operator feature flags. The master
// gates every LLM-driven surface app-wide; five sub-flags carve
// gates every LLM-driven surface app-wide; four sub-flags carve
// the visibility cut per surface (Coach drawer + chat, Daily
// Briefing + advisor, per-metric status cards, correlation
// narration, Health-Score delta explainer). Defaults preserve the
// narration). Defaults preserve the
// v1.4.30 behaviour (every surface visible) so upgrades require
// no admin action. The runtime resolver in
// `src/lib/feature-flags/index.ts` forces every sub-flag false
@@ -4376,12 +4376,11 @@ model AppSettings {
// flag matrix gates server-routed AND iOS on-device surfaces, so
// operators can opt out of every assistant surface regardless of
// where the model runs.
assistantEnabled Boolean @default(true) @map("assistant_enabled")
assistantCoachEnabled Boolean @default(true) @map("assistant_coach_enabled")
assistantBriefingEnabled Boolean @default(true) @map("assistant_briefing_enabled")
assistantInsightStatusEnabled Boolean @default(true) @map("assistant_insight_status_enabled")
assistantCorrelationsEnabled Boolean @default(true) @map("assistant_correlations_enabled")
assistantHealthScoreExplainerEnabled Boolean @default(true) @map("assistant_health_score_explainer_enabled")
assistantEnabled Boolean @default(true) @map("assistant_enabled")
assistantCoachEnabled Boolean @default(true) @map("assistant_coach_enabled")
assistantBriefingEnabled Boolean @default(true) @map("assistant_briefing_enabled")
assistantInsightStatusEnabled Boolean @default(true) @map("assistant_insight_status_enabled")
assistantCorrelationsEnabled Boolean @default(true) @map("assistant_correlations_enabled")
// v1.18.0 — operator-level server-wide module availability. A DISABLED
// allowlist mirroring the per-user `User.modulePreferencesJson`: absent /
@@ -391,8 +391,6 @@ const handler = apiHandler(
settings.assistantInsightStatusEnabled,
assistantCorrelationsEnabled:
settings.assistantCorrelationsEnabled,
assistantHealthScoreExplainerEnabled:
settings.assistantHealthScoreExplainerEnabled,
moduleAvailabilityJson: settings.moduleAvailabilityJson as never,
documentMaxFileBytes: settings.documentMaxFileBytes,
documentQuotaBytes: BigInt(settings.documentQuotaBytes),
@@ -46,7 +46,6 @@ const ALL_ON = {
assistantBriefingEnabled: true,
assistantInsightStatusEnabled: true,
assistantCorrelationsEnabled: true,
assistantHealthScoreExplainerEnabled: true,
};
beforeEach(() => {
@@ -140,7 +139,6 @@ describe("PUT /api/admin/settings/assistant-flags", () => {
expect(body.data.resolved.briefing).toBe(false);
expect(body.data.resolved.insightStatus).toBe(false);
expect(body.data.resolved.correlations).toBe(false);
expect(body.data.resolved.healthScoreExplainer).toBe(false);
});
it("rejects an empty body", async () => {
@@ -1,6 +1,6 @@
/**
* `PUT /api/admin/settings/assistant-flags` — operator-side flip
* surface for the six assistant feature flags.
* surface for the five assistant feature flags.
*
* Dedicated endpoint (separate from the generic
* `/api/admin/settings` PUT) so the admin panel can wire its
@@ -47,7 +47,6 @@ const assistantFlagsSchema = z
assistantBriefingEnabled: z.boolean().optional(),
assistantInsightStatusEnabled: z.boolean().optional(),
assistantCorrelationsEnabled: z.boolean().optional(),
assistantHealthScoreExplainerEnabled: z.boolean().optional(),
})
.strict();
@@ -57,7 +56,6 @@ type AssistantFlagsRow = {
assistantBriefingEnabled: boolean;
assistantInsightStatusEnabled: boolean;
assistantCorrelationsEnabled: boolean;
assistantHealthScoreExplainerEnabled: boolean;
};
function buildResponseShape(row: AssistantFlagsRow) {
@@ -67,7 +65,6 @@ function buildResponseShape(row: AssistantFlagsRow) {
briefing: row.assistantBriefingEnabled,
insightStatus: row.assistantInsightStatusEnabled,
correlations: row.assistantCorrelationsEnabled,
healthScoreExplainer: row.assistantHealthScoreExplainerEnabled,
});
return {
raw: {
@@ -76,8 +73,6 @@ function buildResponseShape(row: AssistantFlagsRow) {
assistantBriefingEnabled: row.assistantBriefingEnabled,
assistantInsightStatusEnabled: row.assistantInsightStatusEnabled,
assistantCorrelationsEnabled: row.assistantCorrelationsEnabled,
assistantHealthScoreExplainerEnabled:
row.assistantHealthScoreExplainerEnabled,
},
resolved,
};
@@ -97,7 +92,6 @@ export const GET = apiHandler(async () => {
assistantBriefingEnabled: true,
assistantInsightStatusEnabled: true,
assistantCorrelationsEnabled: true,
assistantHealthScoreExplainerEnabled: true,
},
});
@@ -109,8 +103,6 @@ export const GET = apiHandler(async () => {
settings?.assistantInsightStatusEnabled ?? true,
assistantCorrelationsEnabled:
settings?.assistantCorrelationsEnabled ?? true,
assistantHealthScoreExplainerEnabled:
settings?.assistantHealthScoreExplainerEnabled ?? true,
};
return apiSuccess(buildResponseShape(row));
@@ -162,8 +154,6 @@ export const PUT = apiHandler(async (request: NextRequest) => {
assistantBriefingEnabled: settings.assistantBriefingEnabled,
assistantInsightStatusEnabled: settings.assistantInsightStatusEnabled,
assistantCorrelationsEnabled: settings.assistantCorrelationsEnabled,
assistantHealthScoreExplainerEnabled:
settings.assistantHealthScoreExplainerEnabled,
};
return apiSuccess(buildResponseShape(row));
@@ -72,7 +72,6 @@ describe("GET /api/feature-flags", () => {
briefing: boolean;
insightStatus: boolean;
correlations: boolean;
healthScoreExplainer: boolean;
};
};
error: null;
@@ -85,7 +84,6 @@ describe("GET /api/feature-flags", () => {
briefing: true,
insightStatus: true,
correlations: true,
healthScoreExplainer: true,
});
});
@@ -97,7 +95,6 @@ describe("GET /api/feature-flags", () => {
assistantBriefingEnabled: true,
assistantInsightStatusEnabled: true,
assistantCorrelationsEnabled: true,
assistantHealthScoreExplainerEnabled: true,
});
const res = await GET(req());
@@ -113,7 +110,6 @@ describe("GET /api/feature-flags", () => {
briefing: false,
insightStatus: false,
correlations: false,
healthScoreExplainer: false,
});
});
@@ -125,7 +121,6 @@ describe("GET /api/feature-flags", () => {
assistantBriefingEnabled: true,
assistantInsightStatusEnabled: false,
assistantCorrelationsEnabled: true,
assistantHealthScoreExplainerEnabled: false,
});
const res = await GET(req());
@@ -141,7 +136,6 @@ describe("GET /api/feature-flags", () => {
briefing: true,
insightStatus: false,
correlations: true,
healthScoreExplainer: false,
});
});
+1 -2
View File
@@ -13,8 +13,7 @@
* "coach": true,
* "briefing": true,
* "insightStatus": true,
* "correlations": true,
* "healthScoreExplainer": true
* "correlations": true
* }
* },
* "error": null
@@ -23,7 +23,7 @@ import { describe, expect, it } from "vitest";
* `requireAssistantSurface("coach")`.
* 2. Other-gated — file invokes `requireAssistantSurface()` with a
* different surface (`insightStatus`,
* `correlations`, `briefing`, `healthScoreExplainer`).
* `correlations`, `briefing`).
* 3. Allowlisted — non-Coach Insights routes that don't gate on
* the matrix at all (provider chain, settings,
* targets, glp1-timeline, feedback). Allowlist
@@ -129,7 +129,6 @@ describe("GET /api/insights/biomarker-assessment", () => {
assistantBriefingEnabled: true,
assistantInsightStatusEnabled: false,
assistantCorrelationsEnabled: true,
assistantHealthScoreExplainerEnabled: true,
} as never);
const res = await callGet(makeReq("bm-1"));
expect(res.status).toBe(403);
@@ -91,7 +91,6 @@ describe("GET /api/insights/blood-pressure-status — assistant-flag gate", () =
assistantBriefingEnabled: true,
assistantInsightStatusEnabled: false,
assistantCorrelationsEnabled: true,
assistantHealthScoreExplainerEnabled: true,
} as never);
const res = await callGet(makeReq());
expect(res.status).toBe(403);
@@ -95,7 +95,6 @@ describe("GET /api/insights/cards — assistant-flag gate", () => {
assistantBriefingEnabled: true,
assistantInsightStatusEnabled: false,
assistantCorrelationsEnabled: true,
assistantHealthScoreExplainerEnabled: true,
} as never);
const res = await callGet(makeReq());
expect(res.status).toBe(403);
@@ -111,7 +110,6 @@ describe("GET /api/insights/cards — assistant-flag gate", () => {
assistantBriefingEnabled: true,
assistantInsightStatusEnabled: true,
assistantCorrelationsEnabled: true,
assistantHealthScoreExplainerEnabled: true,
} as never);
const res = await callGet(makeReq());
expect(res.status).toBe(403);
@@ -167,7 +167,6 @@ describe("GET /api/insights/correlations", () => {
assistantBriefingEnabled: true,
assistantInsightStatusEnabled: true,
assistantCorrelationsEnabled: false,
assistantHealthScoreExplainerEnabled: true,
});
const res = await callGet(makeReq());
expect(res.status).toBe(403);
@@ -185,7 +184,6 @@ describe("GET /api/insights/correlations", () => {
assistantBriefingEnabled: true,
assistantInsightStatusEnabled: true,
assistantCorrelationsEnabled: true,
assistantHealthScoreExplainerEnabled: true,
});
const res = await callGet(makeReq());
expect(res.status).toBe(403);
@@ -134,7 +134,6 @@ describe("GET /api/insights/metric-status", () => {
assistantBriefingEnabled: true,
assistantInsightStatusEnabled: false,
assistantCorrelationsEnabled: true,
assistantHealthScoreExplainerEnabled: true,
} as never);
const res = await callGet(makeReq("SLEEP_DURATION"));
expect(res.status).toBe(403);
@@ -102,7 +102,6 @@ describe("POST /api/insights/pregenerate", () => {
assistantBriefingEnabled: true,
assistantInsightStatusEnabled: true,
assistantCorrelationsEnabled: true,
assistantHealthScoreExplainerEnabled: true,
} as never);
const res = await callPost(makeReq());
expect(res.status).toBe(200);
@@ -117,7 +116,6 @@ describe("POST /api/insights/pregenerate", () => {
assistantBriefingEnabled: true,
assistantInsightStatusEnabled: false,
assistantCorrelationsEnabled: true,
assistantHealthScoreExplainerEnabled: true,
} as never);
const res = await callPost(makeReq());
expect(res.status).toBe(403);
+3 -17
View File
@@ -12,11 +12,10 @@ import { SettingsToggle } from "./_shared";
import { apiGet, apiPut } from "@/lib/api/api-fetch";
/**
* v1.4.31 — operator-side panel for the six assistant feature
* flags. The master toggle gates the whole assistant; five
* v1.4.31 — operator-side panel for the five assistant feature
* flags. The master toggle gates the whole assistant; four
* sub-toggles carve specific surfaces (Coach, Daily Briefing,
* per-metric status cards, correlation narration, Health-Score
* delta explainer).
* per-metric status cards, correlation narration).
*
* UX:
* - Master toggle at the top. When off, the sub-toggles are
@@ -35,7 +34,6 @@ interface AssistantFlagsResponse {
assistantBriefingEnabled: boolean;
assistantInsightStatusEnabled: boolean;
assistantCorrelationsEnabled: boolean;
assistantHealthScoreExplainerEnabled: boolean;
};
resolved: {
enabled: boolean;
@@ -43,7 +41,6 @@ interface AssistantFlagsResponse {
briefing: boolean;
insightStatus: boolean;
correlations: boolean;
healthScoreExplainer: boolean;
};
}
@@ -154,17 +151,6 @@ export function AssistantSection() {
}
disabled={disabledSubs}
/>
<SettingsToggle
label={t("admin.assistant.healthScoreExplainer.title")}
description={t("admin.assistant.healthScoreExplainer.description")}
checked={raw?.assistantHealthScoreExplainerEnabled ?? true}
onCheckedChange={(checked) =>
mutation.mutate({
assistantHealthScoreExplainerEnabled: checked,
})
}
disabled={disabledSubs}
/>
</div>
</div>
</SettingsCard>
@@ -33,7 +33,6 @@ function makeClient(flags: {
briefing: boolean;
insightStatus: boolean;
correlations: boolean;
healthScoreExplainer: boolean;
}): QueryClient {
const client = new QueryClient({
defaultOptions: { queries: { retry: 0 } },
@@ -52,7 +51,6 @@ describe("<InsightStatusCard> — assistant.insightStatus gate", () => {
briefing: true,
insightStatus: true,
correlations: true,
healthScoreExplainer: true,
}),
);
expect(html).toContain("Your pulse is stable");
@@ -67,7 +65,6 @@ describe("<InsightStatusCard> — assistant.insightStatus gate", () => {
briefing: true,
insightStatus: false,
correlations: true,
healthScoreExplainer: true,
}),
);
expect(html).toBe("");
@@ -82,7 +79,6 @@ describe("<InsightStatusCard> — assistant.insightStatus gate", () => {
briefing: false,
insightStatus: false,
correlations: false,
healthScoreExplainer: false,
}),
);
expect(html).toBe("");
@@ -74,7 +74,6 @@ const ALL_ON_FLAGS: AssistantFlagSet = {
briefing: true,
insightStatus: true,
correlations: true,
healthScoreExplainer: true,
};
const flagsSpy = vi.fn<() => AssistantFlagSet>(() => ALL_ON_FLAGS);
vi.mock("@/hooks/use-feature-flags", () => ({
@@ -54,7 +54,6 @@ describe("useFeatureFlags", () => {
briefing: true,
insightStatus: false,
correlations: true,
healthScoreExplainer: false,
},
},
}),
@@ -75,7 +74,6 @@ describe("useFeatureFlags", () => {
briefing: true,
insightStatus: false,
correlations: true,
healthScoreExplainer: false,
},
});
@@ -86,6 +84,5 @@ describe("useFeatureFlags", () => {
);
expect(html).toContain("&quot;coach&quot;:false");
expect(html).toContain("&quot;insightStatus&quot;:false");
expect(html).toContain("&quot;healthScoreExplainer&quot;:false");
});
});
+1 -4
View File
@@ -13,7 +13,7 @@ import { useQueryClientMounted } from "@/hooks/_internal/use-query-client-safe";
*
* The matrix gates every assistant-driven surface on the web —
* Coach + chat, Daily Briefing, per-metric status cards, correlation
* narration, Health-Score delta explainer. iOS reads the same
* narration. iOS reads the same
* endpoint and uses the same matrix; the contract is locked per
* `.planning/RESPONSE-TO-IOS-TEAM-2026-05-16.md` §3 R5.
*/
@@ -29,8 +29,6 @@ export interface AssistantFlagSet {
insightStatus: boolean;
/** Correlation narration tile on the mother page. */
correlations: boolean;
/** `?` glyph that opens the Health-Score delta explainer popover. */
healthScoreExplainer: boolean;
}
interface FeatureFlagsPayload {
@@ -44,7 +42,6 @@ export const DEFAULT_ASSISTANT_FLAGS: AssistantFlagSet = Object.freeze({
briefing: true,
insightStatus: true,
correlations: true,
healthScoreExplainer: true,
});
async function fetchFeatureFlags(): Promise<FeatureFlagsPayload> {
@@ -215,7 +215,6 @@ beforeEach(() => {
briefing: true,
insightStatus: true,
correlations: true,
healthScoreExplainer: true,
});
fakePrisma.measurement.findMany.mockResolvedValue([]);
fakePrisma.moodEntry.findMany.mockResolvedValue([]);
@@ -652,7 +651,6 @@ describe("buildDashboardSnapshot — briefingState matrix", () => {
briefing: false,
insightStatus: false,
correlations: false,
healthScoreExplainer: false,
});
const snap = await buildDashboardSnapshot(
fakePrisma,
@@ -74,7 +74,6 @@ const appSettings = {
assistantBriefingEnabled: false,
assistantInsightStatusEnabled: false,
assistantCorrelationsEnabled: false,
assistantHealthScoreExplainerEnabled: false,
moduleAvailabilityJson: { nutrients: false, insights: true },
documentMaxFileBytes: 12_345_678,
documentQuotaBytes: BigInt("9876543210"),
@@ -82,7 +82,6 @@ function buildClient(flags: Partial<AssistantFlagSet>): QueryClient {
briefing: true,
insightStatus: true,
correlations: true,
healthScoreExplainer: true,
...flags,
},
});
@@ -43,7 +43,6 @@ describe("resolveAssistantFlags", () => {
briefing: false,
insightStatus: true,
correlations: false,
healthScoreExplainer: true,
};
expect(resolveAssistantFlags(input)).toEqual(input);
});
@@ -55,7 +54,6 @@ describe("resolveAssistantFlags", () => {
briefing: true,
insightStatus: true,
correlations: true,
healthScoreExplainer: true,
};
expect(resolveAssistantFlags(input)).toEqual({
enabled: false,
@@ -63,7 +61,6 @@ describe("resolveAssistantFlags", () => {
briefing: false,
insightStatus: false,
correlations: false,
healthScoreExplainer: false,
});
});
});
@@ -82,7 +79,6 @@ describe("getAssistantFlags", () => {
assistantBriefingEnabled: true,
assistantInsightStatusEnabled: false,
assistantCorrelationsEnabled: true,
assistantHealthScoreExplainerEnabled: false,
});
const flags = await getAssistantFlags();
expect(flags).toEqual({
@@ -91,7 +87,6 @@ describe("getAssistantFlags", () => {
briefing: true,
insightStatus: false,
correlations: true,
healthScoreExplainer: false,
});
});
@@ -102,7 +97,6 @@ describe("getAssistantFlags", () => {
assistantBriefingEnabled: true,
assistantInsightStatusEnabled: true,
assistantCorrelationsEnabled: true,
assistantHealthScoreExplainerEnabled: true,
});
const flags = await getAssistantFlags();
expect(flags).toEqual({
@@ -111,7 +105,6 @@ describe("getAssistantFlags", () => {
briefing: false,
insightStatus: false,
correlations: false,
healthScoreExplainer: false,
});
});
@@ -130,7 +123,6 @@ describe("requireAssistantSurface", () => {
assistantBriefingEnabled: true,
assistantInsightStatusEnabled: true,
assistantCorrelationsEnabled: true,
assistantHealthScoreExplainerEnabled: true,
});
const flags = await requireAssistantSurface("coach");
expect(flags.coach).toBe(true);
@@ -143,7 +135,6 @@ describe("requireAssistantSurface", () => {
assistantBriefingEnabled: true,
assistantInsightStatusEnabled: true,
assistantCorrelationsEnabled: true,
assistantHealthScoreExplainerEnabled: true,
});
await expect(requireAssistantSurface("coach")).rejects.toThrow(
AssistantDisabledError,
@@ -157,7 +148,6 @@ describe("requireAssistantSurface", () => {
assistantBriefingEnabled: true,
assistantInsightStatusEnabled: true,
assistantCorrelationsEnabled: true,
assistantHealthScoreExplainerEnabled: true,
});
await expect(requireAssistantSurface("briefing")).rejects.toThrow(
AssistantDisabledError,
@@ -184,7 +174,6 @@ describe("per-request memoisation", () => {
assistantBriefingEnabled: true,
assistantInsightStatusEnabled: true,
assistantCorrelationsEnabled: true,
assistantHealthScoreExplainerEnabled: true,
});
// Pin a stable stub object so every `getEvent()` inside this test
// returns the same reference — the memo keys on identity, so a
@@ -207,7 +196,6 @@ describe("per-request memoisation", () => {
assistantBriefingEnabled: true,
assistantInsightStatusEnabled: true,
assistantCorrelationsEnabled: true,
assistantHealthScoreExplainerEnabled: true,
});
mockEvent = { addWarning: vi.fn(), id: "req-1" };
@@ -0,0 +1,137 @@
/**
* Every assistant switch an operator can flip has to move something.
*
* `healthScoreExplainer` did not. It gated a caption beside the
* Health-Score delta; the caption's component went when the health
* score was replaced by the reference composite, and the "?" control
* the admin description still named had been retired a release before
* that. The toggle stayed on the admin panel, in the flag matrix and in
* the API contract for two releases, so an operator could turn it off,
* be told it saved, and change nothing at all. A switch that lies is
* worse than no switch.
*
* Two guards, one point. The first pins the removal so the flag cannot
* drift back in pieces. The second is the general rule the removal came
* from: no sub-flag is allowed to exist without a surface that reads it.
* Plumbing does not count — the admin panel, the resolver, the hook and
* the two routes carry every flag by construction, so a dead one would
* look alive if they were allowed to vouch for it.
*/
import { readdirSync, readFileSync, statSync } from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
ASSISTANT_FLAGS_DEFAULT,
resolveAssistantFlags,
} from "@/lib/feature-flags";
const ROOT = path.resolve(__dirname, "../../../..");
const LOCALES = ["de", "en", "es", "fr", "it", "pl"] as const;
/** Names the retired switch went by, on the wire and in the database. */
const RETIRED_NAMES = [
"healthScoreExplainer",
"assistantHealthScoreExplainerEnabled",
"assistant_health_score_explainer_enabled",
];
/**
* The files that carry every flag whatever it does: the operator panel
* that offers the switch, the resolver and hook that shape the matrix,
* and the two routes that read and write it. None of them is evidence
* that a flag gates anything.
*/
const PLUMBING = [
"src/lib/feature-flags/index.ts",
"src/hooks/use-feature-flags.ts",
"src/components/admin/assistant-section.tsx",
"src/app/api/feature-flags/route.ts",
"src/app/api/admin/settings/assistant-flags/route.ts",
];
function sourceFiles(dir: string, acc: string[] = []): string[] {
for (const entry of readdirSync(dir)) {
if (entry === "generated" || entry === "__tests__") continue;
const full = path.join(dir, entry);
if (statSync(full).isDirectory()) {
sourceFiles(full, acc);
} else if (/\.tsx?$/.test(entry)) {
acc.push(full);
}
}
return acc;
}
describe("the Health-Score explainer switch stays removed", () => {
it("is absent from the resolved flag matrix, master on and master off", () => {
const on = resolveAssistantFlags({ ...ASSISTANT_FLAGS_DEFAULT });
const off = resolveAssistantFlags({
...ASSISTANT_FLAGS_DEFAULT,
enabled: false,
});
for (const shape of [ASSISTANT_FLAGS_DEFAULT, on, off]) {
expect(Object.keys(shape)).not.toContain("healthScoreExplainer");
}
});
it("declares no column on the Prisma schema", () => {
const schema = readFileSync(
path.join(ROOT, "prisma/schema.prisma"),
"utf8",
);
for (const name of RETIRED_NAMES) {
expect(schema, `${name} is back on the schema`).not.toContain(name);
}
});
it("carries no operator-panel wording in any locale", () => {
for (const locale of LOCALES) {
const bundle = JSON.parse(
readFileSync(path.join(ROOT, `messages/${locale}.json`), "utf8"),
) as { admin: { assistant: Record<string, unknown> } };
expect(
Object.keys(bundle.admin.assistant),
`${locale} still offers the switch`,
).not.toContain("healthScoreExplainer");
}
});
it("ships the migration that dropped the column", () => {
const sql = readFileSync(
path.join(
ROOT,
"prisma/migrations/0288_drop_health_score_explainer_flag/migration.sql",
),
"utf8",
);
expect(sql).toContain("assistant_health_score_explainer_enabled");
expect(sql).toContain("DROP COLUMN");
});
});
describe("no assistant sub-flag is a dead switch", () => {
it("finds a surface that reads every flag the operator can turn off", () => {
const plumbing = new Set(PLUMBING.map((rel) => path.join(ROOT, rel)));
const sources = sourceFiles(path.join(ROOT, "src")).filter(
(file) => !plumbing.has(file),
);
const corpus = sources.map((file) => readFileSync(file, "utf8")).join("\n");
const subFlags = Object.keys(ASSISTANT_FLAGS_DEFAULT).filter(
(flag) => flag !== "enabled",
);
expect(subFlags.length).toBeGreaterThan(0);
for (const flag of subFlags) {
const gatedOnTheServer = corpus.includes(
`requireAssistantSurface("${flag}")`,
);
const readOnTheClient = new RegExp(`flags\\.${flag}\\b`).test(corpus);
expect(
gatedOnTheServer || readOnTheClient,
`the "${flag}" switch gates nothing — wire it to a surface or remove it`,
).toBe(true);
}
});
});
+5 -16
View File
@@ -5,9 +5,9 @@ import { memoizePerRequest } from "@/lib/request-cache";
/**
* v1.4.31 — Assistant-surface operator feature flags.
*
* Six boolean toggles on `AppSettings` carve the visibility cut for
* the seven LLM-driven surfaces. The master flag is a single kill-
* switch; the five sub-flags carve specific surfaces.
* Five boolean toggles on `AppSettings` carve the visibility cut for
* the LLM-driven surfaces. The master flag is a single kill-switch;
* the four sub-flags carve specific surfaces.
*
* `assistant.enabled = false` forces every sub-flag false in the
* resolved shape — the master always wins. This means a server-side
@@ -20,13 +20,9 @@ import { memoizePerRequest } from "@/lib/request-cache";
* shape so iOS reads the same authoritative set the web reads.
*/
/** The five assistant sub-flags the operator can carve. */
/** The four assistant sub-flags the operator can carve. */
export type AssistantSurface =
| "coach"
| "briefing"
| "insightStatus"
| "correlations"
| "healthScoreExplainer";
"coach" | "briefing" | "insightStatus" | "correlations";
export interface AssistantFlagSet {
/** Master kill-switch — when false, every sub-flag is forced false. */
@@ -39,8 +35,6 @@ export interface AssistantFlagSet {
insightStatus: boolean;
/** Correlation narration tile on the mother page. */
correlations: boolean;
/** `?` glyph that opens the Health-Score delta explainer popover. */
healthScoreExplainer: boolean;
}
/** All-on default; mirrors v1.4.30 behaviour for fresh installs. */
@@ -50,7 +44,6 @@ export const ASSISTANT_FLAGS_DEFAULT: AssistantFlagSet = Object.freeze({
briefing: true,
insightStatus: true,
correlations: true,
healthScoreExplainer: true,
});
/**
@@ -80,7 +73,6 @@ export async function getAssistantFlags(): Promise<AssistantFlagSet> {
assistantBriefingEnabled: true,
assistantInsightStatusEnabled: true,
assistantCorrelationsEnabled: true,
assistantHealthScoreExplainerEnabled: true,
},
});
@@ -91,8 +83,6 @@ export async function getAssistantFlags(): Promise<AssistantFlagSet> {
briefing: settings?.assistantBriefingEnabled ?? true,
insightStatus: settings?.assistantInsightStatusEnabled ?? true,
correlations: settings?.assistantCorrelationsEnabled ?? true,
healthScoreExplainer:
settings?.assistantHealthScoreExplainerEnabled ?? true,
});
} catch {
getEvent()?.addWarning(
@@ -116,7 +106,6 @@ export function resolveAssistantFlags(raw: AssistantFlagSet): AssistantFlagSet {
briefing: false,
insightStatus: false,
correlations: false,
healthScoreExplainer: false,
};
}
return { ...raw };
-2
View File
@@ -25,7 +25,6 @@ vi.mock("@/lib/feature-flags", () => ({
briefing: true,
insightStatus: true,
correlations: true,
healthScoreExplainer: true,
})),
}));
// v1.22.0 — `search` reads the record directly via Prisma; stub it so the
@@ -1098,7 +1097,6 @@ describe("get_ecg_recordings — v1.30 coverage review (G3)", () => {
briefing: true,
insightStatus: false,
correlations: true,
healthScoreExplainer: true,
} as never);
const result = (await tool("get_ecg_recordings").run(CTX, {})) as {
present: boolean;
-1
View File
@@ -101,6 +101,5 @@ export const adminSettingsSchema = z
assistantBriefingEnabled: z.boolean().optional(),
assistantInsightStatusEnabled: z.boolean().optional(),
assistantCorrelationsEnabled: z.boolean().optional(),
assistantHealthScoreExplainerEnabled: z.boolean().optional(),
})
.strict();
-1
View File
@@ -595,7 +595,6 @@ const appSettingsBackupSchema = z
assistantBriefingEnabled: z.boolean(),
assistantInsightStatusEnabled: z.boolean(),
assistantCorrelationsEnabled: z.boolean(),
assistantHealthScoreExplainerEnabled: z.boolean(),
moduleAvailabilityJson: z.unknown().nullable(),
documentMaxFileBytes: z.number().int(),
documentQuotaBytes: z.string().regex(/^\d+$/),
@@ -155,7 +155,6 @@ describe("canonical disaster-recovery backup round-trip", () => {
assistantBriefingEnabled: false,
assistantInsightStatusEnabled: false,
assistantCorrelationsEnabled: false,
assistantHealthScoreExplainerEnabled: false,
moduleAvailabilityJson: { insights: true, nutrients: false },
documentMaxFileBytes: 12_345_678,
documentQuotaBytes: BigInt("9876543210"),