From ba36e46c42a58fceb520ac36504c07fd1f589273 Mon Sep 17 00:00:00 2001 From: Chris Date: Thu, 16 Jul 2026 06:02:02 -0400 Subject: [PATCH] feat(drug-reference): gate affirmative remedy content behind an off-by-default flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add drugReference.remediesEnabled (default off), independent of the tier install. When off, the server emits no remedy data at any boundary — the drug-reference page prop, conditions show, and the /api/conditions/drugs situation search — and the "Natural" filter is hidden, so installing the medicine-standard tier lights up the verbatim FDA label search and the condition-to-OTC matching but not the hand-authored self-care and herbal sections. No user-facing toggle: it is flipped on after a clinician content-pass. Implements the upstream #1040 split-by-risk request: the regulated label content ships with the tier; the authored remedy guidance stays gated until sign-off. Co-Authored-By: Claude Opus 4.8 --- .../app/controllers/conditions_controller.ts | 18 ++++++++++--- .../controllers/drug_reference_controller.ts | 11 ++++++-- admin/app/utils/affirmative_remedies.ts | 23 ++++++++++++++++ admin/inertia/pages/drug-reference/index.tsx | 26 +++++++++++++------ admin/types/kv_store.ts | 6 +++++ 5 files changed, 70 insertions(+), 14 deletions(-) create mode 100644 admin/app/utils/affirmative_remedies.ts diff --git a/admin/app/controllers/conditions_controller.ts b/admin/app/controllers/conditions_controller.ts index d2a1a44..b77cc55 100644 --- a/admin/app/controllers/conditions_controller.ts +++ b/admin/app/controllers/conditions_controller.ts @@ -2,6 +2,7 @@ import type { HttpContext } from '@adonisjs/core/http' import logger from '@adonisjs/core/services/logger' import { ConditionService } from '#services/condition_service' import { conditionDrugsValidator } from '#validators/conditions' +import { affirmativeRemediesEnabled } from '../utils/affirmative_remedies.js' /** * "When to use what" — condition-first HTTP boundary (Phase 1). @@ -46,15 +47,18 @@ export default class ConditionsController { return response.notFound({ error: 'Condition not found' }) } - const [result, drugRowCount] = await Promise.all([ + const [result, drugRowCount, remediesOn] = await Promise.all([ this.service.drugsForSlug(slug), this.service.drugRowCount(), + affirmativeRemediesEnabled(), ]) return inertia.render('conditions/show', { condition: result?.condition ?? null, drugs: result?.drugs ?? [], - remedies: result?.remedies ?? [], + // Affirmative remedies gated off by default (#1040); the OTC/condition + // match above is regulated label text and stays live. + remedies: remediesOn ? (result?.remedies ?? []) : [], drugRowCount, }) } catch (err) { @@ -82,16 +86,22 @@ export default class ConditionsController { sort: params.sort, } + // Strip affirmative remedies from the situation-search response when the + // gate is closed (#1040); the OTC drug matches are regulated label text and + // are returned either way. + const remediesOn = await affirmativeRemediesEnabled() + if (params.slug) { const result = await this.service.drugsForSlug(params.slug, params.limit, filterOpts) if (!result) { return response.notFound({ error: 'Condition not found' }) } - return result + return remediesOn ? result : { ...result, remedies: [] } } if (params.q) { - return await this.service.drugsForFreeText(params.q, params.limit, filterOpts) + const result = await this.service.drugsForFreeText(params.q, params.limit, filterOpts) + return remediesOn ? result : { ...result, remedies: [] } } return response.badRequest({ error: 'Provide a slug or q query parameter' }) diff --git a/admin/app/controllers/drug_reference_controller.ts b/admin/app/controllers/drug_reference_controller.ts index 7acf29f..c9ad7bf 100644 --- a/admin/app/controllers/drug_reference_controller.ts +++ b/admin/app/controllers/drug_reference_controller.ts @@ -5,6 +5,7 @@ import { ConditionService } from '#services/condition_service' import { searchDrugValidator, interactionsValidator } from '#validators/drug_reference' import { parseCompareIds } from '../../util/compare_ids.js' import { situationsForIndications } from '../../util/conditions.js' +import { affirmativeRemediesEnabled } from '../utils/affirmative_remedies.js' /** * Drug Reference v1 — HTTP boundary. @@ -31,16 +32,21 @@ export default class DrugReferenceController { async index({ inertia }: HttpContext) { try { const conditionService = new ConditionService() - const [status, count] = await Promise.all([ + const [status, count, remediesOn] = await Promise.all([ this.service.getIngestStatus(), this.service.rowCount(), + affirmativeRemediesEnabled(), ]) return inertia.render('drug-reference/index', { ingestStatus: status, rowCount: count, conditions: conditionService.listConditions(), - remedies: conditionService.listRemedies(), + // Affirmative remedy content is gated off by default (#1040): keep it out + // of the payload entirely when disabled, and tell the page so it can hide + // the "Natural" filter too. Drug search + condition matching are unaffected. + remedies: remediesOn ? conditionService.listRemedies() : [], + remediesEnabled: remediesOn, }) } catch (err) { const msg = err instanceof Error ? err.message : String(err) @@ -50,6 +56,7 @@ export default class DrugReferenceController { rowCount: 0, conditions: [], remedies: [], + remediesEnabled: false, }) } } diff --git a/admin/app/utils/affirmative_remedies.ts b/admin/app/utils/affirmative_remedies.ts new file mode 100644 index 0000000..e37f0af --- /dev/null +++ b/admin/app/utils/affirmative_remedies.ts @@ -0,0 +1,23 @@ +import KVStore from '#models/kv_store' + +/** + * Affirmative-content gate for the drug-reference feature (upstream #1040). + * + * The verbatim FDA label search and the condition→OTC matching are grounded in + * regulated label text and ship with the tier. The hand-authored self-care and + * herbal REMEDY content is guidance we author, so it stays gated off by default + * until a clinician has done the content pass; the maintainer flips this on in a + * follow-up once that's signed off. + * + * Independent of the tier install-state (a `medicine-standard` install alone does + * NOT enable remedies). Defaults off — a missing/null KV value reads as false — + * and there is deliberately no user-facing toggle, so un-reviewed medical + * guidance can't be self-enabled. + * + * Read at every HTTP boundary that could emit remedy data (the drug-reference + * page prop and the conditions show / drugs API), so no affirmative content is + * serialized to the client while the gate is closed. + */ +export async function affirmativeRemediesEnabled(): Promise { + return (await KVStore.getValue('drugReference.remediesEnabled')) === true +} diff --git a/admin/inertia/pages/drug-reference/index.tsx b/admin/inertia/pages/drug-reference/index.tsx index 5fb5ed9..0efed4d 100644 --- a/admin/inertia/pages/drug-reference/index.tsx +++ b/admin/inertia/pages/drug-reference/index.tsx @@ -20,6 +20,12 @@ interface PageProps { rowCount: number conditions: ConditionSummary[] remedies: NaturalRemedy[] + /** + * Affirmative-content gate (#1040). False by default: the server sends no + * remedy data and the "Natural" filter is hidden, so only FDA label search and + * condition→OTC matching show. Flipped on after a clinician content-pass. + */ + remediesEnabled?: boolean } /** @@ -113,7 +119,7 @@ function drugKey(d: DrugSearchResult): string { * Once data is loaded: chips + dual-section results, with the FDA-data update control * and source citation at the foot. */ -export default function DrugReferenceIndex({ ingestStatus, rowCount, conditions, remedies = [] }: PageProps) { +export default function DrugReferenceIndex({ ingestStatus, rowCount, conditions, remedies = [], remediesEnabled = false }: PageProps) { const [query, setQuery] = useState('') const [productType, setProductType] = useState(null) const [route, setRoute] = useState(null) @@ -563,13 +569,17 @@ export default function DrugReferenceIndex({ ingestStatus, rowCount, conditions, > Rx - handleFilterChange(NATURAL_FILTER)} - > - Natural - + {/* Affirmative-content gate (#1040): the "Natural" remedy filter + only appears once remedies are enabled (post clinician-pass). */} + {remediesEnabled && ( + handleFilterChange(NATURAL_FILTER)} + > + Natural + + )} {/* Secondary controls: route + sort for the drug-name search, or herb/self-care sub-filter when Natural is active. */} diff --git a/admin/types/kv_store.ts b/admin/types/kv_store.ts index 33a9326..024bc87 100644 --- a/admin/types/kv_store.ts +++ b/admin/types/kv_store.ts @@ -57,6 +57,12 @@ export const KV_STORE_SCHEMA = { // null fallback — the key simply doesn't exist before the first download. // Cleared after a full ingest succeeds (when the on-disk parts are deleted). 'drugReference.downloadState': 'string', + // Drug Reference — affirmative-content gate (upstream #1040). Independent of + // the tier install: installing `medicine-standard` lights up the verbatim FDA + // label search and condition→OTC matching, but the hand-authored self-care and + // herbal REMEDY sections stay hidden until this flips true. Defaults off + // (null → false); flipped on after a clinician content-pass, not user-toggled. + 'drugReference.remediesEnabled': 'boolean', } as const type KVTagToType = T extends 'boolean' ? boolean : string