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:
parent
13183e3ca0
commit
ba36e46c42
|
|
@ -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' })
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<boolean> {
|
||||
return (await KVStore.getValue('drugReference.remediesEnabled')) === true
|
||||
}
|
||||
|
|
@ -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<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. */}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue