feat(drug-reference): gate affirmative remedy content behind an off-by-default flag

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 <noreply@anthropic.com>
This commit is contained in:
Chris 2026-07-16 06:02:02 -04:00
parent 13183e3ca0
commit ba36e46c42
5 changed files with 70 additions and 14 deletions

View File

@ -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' })

View File

@ -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,
})
}
}

View File

@ -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 conditionOTC 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<boolean> {
return (await KVStore.getValue('drugReference.remediesEnabled')) === true
}

View File

@ -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
* conditionOTC 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<string | null>(null)
const [route, setRoute] = useState<string | null>(null)
@ -563,13 +569,17 @@ export default function DrugReferenceIndex({ ingestStatus, rowCount, conditions,
>
Rx
</FilterPill>
<FilterPill
active={productType === NATURAL_FILTER}
tone="olive"
onClick={() => handleFilterChange(NATURAL_FILTER)}
>
Natural
</FilterPill>
{/* Affirmative-content gate (#1040): the "Natural" remedy filter
only appears once remedies are enabled (post clinician-pass). */}
{remediesEnabled && (
<FilterPill
active={productType === NATURAL_FILTER}
tone="olive"
onClick={() => handleFilterChange(NATURAL_FILTER)}
>
Natural
</FilterPill>
)}
{/* Secondary controls: route + sort for the drug-name search, or
herb/self-care sub-filter when Natural is active. */}

View File

@ -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 string> = T extends 'boolean' ? boolean : string