refactor(drug-reference): make collections JSON the single source for curated data (#1130)

Generate app/data/{conditions,natural_remedies,home_remedies}.ts from the
repo-root collections/*.json via `npm run gen:curated-data`, so the JSON is the
only file edited by hand. The generated modules keep the data compiled into the
image (no runtime file read, no path fragility, which is why the data was a TS
constant), and curated_data_sync.standalone.ts fails CI if a generated module
ever drifts from its JSON.

This removes the burden of hand-keeping the .json mirror and the .ts constant in
sync. Follow-up to the review discussion on #1040.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
caweis 2026-07-21 18:50:47 -04:00 committed by jakeaturner
parent 09a651d330
commit 6c0271cd6f
No known key found for this signature in database
GPG Key ID: B1072EBDEECE328D
6 changed files with 509 additions and 301 deletions

View File

@ -1,342 +1,438 @@
/* eslint-disable */
/**
* "When to use what" curated condition spine (Phase 1, runtime source of truth).
* GENERATED FILE DO NOT EDIT.
*
* A bounded, hand-curated list of common first-aid / emergency situations a
* "small booklet" of when-to-use-what. Each entry's `searchTerms` drive the
* FULLTEXT search over `drug_labels.indications`.
* Single source of truth: collections/conditions.json
* Regenerate after editing the JSON: `npm run gen:curated-data` (from admin/).
* curated_data_sync.standalone.ts fails CI if this file drifts from the JSON.
*
* WHY a TS constant (not a JSON file read at runtime):
* The repo-root `collections/conditions.json` mirror exists for parity with
* the kiwix-categories convention and for human discoverability, but it is
* NOT what the running app reads. The Dockerfile copies only `admin/` into the
* image (`ADD admin/ ./`) and ships only the compiled `build/` output, so a
* repo-root JSON file never reaches the container filesystem. Bundling the
* spine as a compiled module guarantees it is always present at runtime with
* no path-resolution fragility.
*
* Keep this file and `collections/conditions.json` in sync (same `version`,
* same entries) the JSON is the public/browseable copy, this is the canonical
* runtime copy. `parseConditionsFile` (admin/util/conditions.ts) validates the
* shape so a hand-edit that breaks an entry degrades to "skip that entry"
* rather than crashing the page.
* Curated "when to use what" condition spine (Phase 1). US-government / public-domain sourcing.
*/
import type { ConditionsFile } from '../../types/conditions.js'
export const CONDITIONS_FILE: ConditionsFile = {
version: '2026-06-07',
conditions: [
// ── Pain, fever & inflammation ──────────────────────────────────────────
"version": "2026-06-07",
"conditions": [
{
slug: 'pain',
label: 'Pain',
category: 'Pain, fever & inflammation',
searchTerms: ['pain', 'aches', 'minor aches', 'pain relief', 'analgesic'],
"slug": "pain",
"label": "Pain",
"category": "Pain, fever & inflammation",
"searchTerms": [
"pain",
"aches",
"minor aches",
"pain relief",
"analgesic"
]
},
{
slug: 'headache',
label: 'Headache',
category: 'Pain, fever & inflammation',
searchTerms: ['headache', 'migraine', 'head pain', 'tension headache'],
"slug": "headache",
"label": "Headache",
"category": "Pain, fever & inflammation",
"searchTerms": [
"headache",
"migraine",
"head pain",
"tension headache"
]
},
{
slug: 'muscle-joint-pain',
label: 'Muscle & joint pain',
category: 'Pain, fever & inflammation',
searchTerms: [
'muscular aches',
'muscle aches',
'backache',
'arthritis',
'joint pain',
'minor pain of arthritis',
],
"slug": "muscle-joint-pain",
"label": "Muscle & joint pain",
"category": "Pain, fever & inflammation",
"searchTerms": [
"muscular aches",
"muscle aches",
"backache",
"arthritis",
"joint pain",
"minor pain of arthritis"
]
},
{
slug: 'fever',
label: 'Fever',
category: 'Pain, fever & inflammation',
searchTerms: ['fever', 'reduces fever', 'fever reducer', 'temporarily reduces fever'],
"slug": "fever",
"label": "Fever",
"category": "Pain, fever & inflammation",
"searchTerms": [
"fever",
"reduces fever",
"fever reducer",
"temporarily reduces fever"
]
},
{
slug: 'menstrual-cramps',
label: 'Menstrual cramps',
category: 'Pain, fever & inflammation',
searchTerms: ['menstrual cramps', 'menstrual pain', 'period pain', 'premenstrual'],
},
// ── Cold, cough & allergy ───────────────────────────────────────────────
{
slug: 'cough',
label: 'Cough',
category: 'Cold, cough & allergy',
searchTerms: ['cough', 'cough suppressant', 'coughing', 'controls cough'],
"slug": "menstrual-cramps",
"label": "Menstrual cramps",
"category": "Pain, fever & inflammation",
"searchTerms": [
"menstrual cramps",
"menstrual pain",
"period pain",
"premenstrual"
]
},
{
slug: 'nasal-congestion',
label: 'Nasal congestion',
category: 'Cold, cough & allergy',
searchTerms: [
'nasal congestion',
'stuffy nose',
'sinus congestion',
'decongestant',
'nasal decongestant',
],
"slug": "cough",
"label": "Cough",
"category": "Cold, cough & allergy",
"searchTerms": [
"cough",
"cough suppressant",
"coughing",
"controls cough"
]
},
{
slug: 'sore-throat',
label: 'Sore throat',
category: 'Cold, cough & allergy',
searchTerms: ['sore throat', 'sore mouth', 'minor sore throat', 'throat pain'],
"slug": "nasal-congestion",
"label": "Nasal congestion",
"category": "Cold, cough & allergy",
"searchTerms": [
"nasal congestion",
"stuffy nose",
"sinus congestion",
"decongestant",
"nasal decongestant"
]
},
{
slug: 'common-cold',
label: 'Common cold',
category: 'Cold, cough & allergy',
searchTerms: ['common cold', 'cold symptoms', 'cold', 'flu symptoms'],
"slug": "sore-throat",
"label": "Sore throat",
"category": "Cold, cough & allergy",
"searchTerms": [
"sore throat",
"sore mouth",
"minor sore throat",
"throat pain"
]
},
{
slug: 'allergic-reaction',
label: 'Allergies & allergic reactions',
category: 'Cold, cough & allergy',
searchTerms: [
'allergy',
'allergic reactions',
'hay fever',
'antihistamine',
'runny nose',
'sneezing',
'itchy watery eyes',
],
},
// ── Stomach & digestion ─────────────────────────────────────────────────
{
slug: 'heartburn',
label: 'Heartburn',
category: 'Stomach & digestion',
searchTerms: ['heartburn', 'acid indigestion', 'acid reducer', 'antacid', 'sour stomach'],
"slug": "common-cold",
"label": "Common cold",
"category": "Cold, cough & allergy",
"searchTerms": [
"common cold",
"cold symptoms",
"cold",
"flu symptoms"
]
},
{
slug: 'indigestion',
label: 'Indigestion & upset stomach',
category: 'Stomach & digestion',
searchTerms: ['indigestion', 'upset stomach', 'gas', 'bloating', 'fullness', 'antacid'],
"slug": "allergic-reaction",
"label": "Allergies & allergic reactions",
"category": "Cold, cough & allergy",
"searchTerms": [
"allergy",
"allergic reactions",
"hay fever",
"antihistamine",
"runny nose",
"sneezing",
"itchy watery eyes"
]
},
{
slug: 'nausea-vomiting',
label: 'Nausea & vomiting',
category: 'Stomach & digestion',
searchTerms: ['nausea', 'vomiting', 'upset stomach associated with nausea'],
"slug": "heartburn",
"label": "Heartburn",
"category": "Stomach & digestion",
"searchTerms": [
"heartburn",
"acid indigestion",
"acid reducer",
"antacid",
"sour stomach"
]
},
{
slug: 'diarrhea',
label: 'Diarrhea',
category: 'Stomach & digestion',
searchTerms: ['diarrhea', 'antidiarrheal', 'loose stools', 'travelers diarrhea'],
"slug": "indigestion",
"label": "Indigestion & upset stomach",
"category": "Stomach & digestion",
"searchTerms": [
"indigestion",
"upset stomach",
"gas",
"bloating",
"fullness",
"antacid"
]
},
{
slug: 'constipation',
label: 'Constipation',
category: 'Stomach & digestion',
searchTerms: [
'constipation',
'laxative',
'irregularity',
'occasional constipation',
'stool softener',
],
"slug": "nausea-vomiting",
"label": "Nausea & vomiting",
"category": "Stomach & digestion",
"searchTerms": [
"nausea",
"vomiting",
"upset stomach associated with nausea"
]
},
{
slug: 'motion-sickness',
label: 'Motion sickness',
category: 'Stomach & digestion',
searchTerms: ['motion sickness', 'travel sickness', 'seasickness', 'car sickness'],
"slug": "diarrhea",
"label": "Diarrhea",
"category": "Stomach & digestion",
"searchTerms": [
"diarrhea",
"antidiarrheal",
"loose stools",
"travelers diarrhea"
]
},
{
slug: 'gas',
label: 'Gas & bloating',
category: 'Stomach & digestion',
searchTerms: ['gas', 'bloating', 'flatulence', 'antigas', 'pressure'],
},
// ── Skin & wounds ───────────────────────────────────────────────────────
{
slug: 'wounds-cuts',
label: 'Wounds & cuts',
category: 'Skin & wounds',
searchTerms: [
'minor cuts',
'scrapes',
'wounds',
'first aid antiseptic',
'first aid to help prevent infection',
'abrasions',
],
"slug": "constipation",
"label": "Constipation",
"category": "Stomach & digestion",
"searchTerms": [
"constipation",
"laxative",
"irregularity",
"occasional constipation",
"stool softener"
]
},
{
slug: 'burns',
label: 'Burns',
category: 'Skin & wounds',
searchTerms: ['burns', 'minor burns', 'sunburn', 'scald', 'minor burn'],
"slug": "motion-sickness",
"label": "Motion sickness",
"category": "Stomach & digestion",
"searchTerms": [
"motion sickness",
"travel sickness",
"seasickness",
"car sickness"
]
},
{
slug: 'insect-bites-stings',
label: 'Insect bites & stings',
category: 'Skin & wounds',
searchTerms: [
'insect bites',
'insect stings',
'bug bites',
'bee sting',
'itching from insect bites',
],
"slug": "gas",
"label": "Gas & bloating",
"category": "Stomach & digestion",
"searchTerms": [
"gas",
"bloating",
"flatulence",
"antigas",
"pressure"
]
},
{
slug: 'skin-rash-itch',
label: 'Rash & itching',
category: 'Skin & wounds',
searchTerms: [
'itching',
'rash',
'skin irritation',
'itchy skin',
'minor skin irritations',
'eczema',
],
"slug": "wounds-cuts",
"label": "Wounds & cuts",
"category": "Skin & wounds",
"searchTerms": [
"minor cuts",
"scrapes",
"wounds",
"first aid antiseptic",
"first aid to help prevent infection",
"abrasions"
]
},
{
slug: 'poison-ivy',
label: 'Poison ivy & plant rashes',
category: 'Skin & wounds',
searchTerms: ['poison ivy', 'poison oak', 'poison sumac', 'rashes due to poison ivy'],
"slug": "burns",
"label": "Burns",
"category": "Skin & wounds",
"searchTerms": [
"burns",
"minor burns",
"sunburn",
"scald",
"minor burn"
]
},
{
slug: 'fungal-infection',
label: 'Athletes foot & ringworm',
category: 'Skin & wounds',
searchTerms: [
'athletes foot',
'ringworm',
'jock itch',
'antifungal',
'fungal infection',
'tinea',
],
"slug": "insect-bites-stings",
"label": "Insect bites & stings",
"category": "Skin & wounds",
"searchTerms": [
"insect bites",
"insect stings",
"bug bites",
"bee sting",
"itching from insect bites"
]
},
{
slug: 'dry-skin',
label: 'Dry & chapped skin',
category: 'Skin & wounds',
searchTerms: ['dry skin', 'chapped skin', 'chapped lips', 'skin protectant', 'cracked skin'],
"slug": "skin-rash-itch",
"label": "Rash & itching",
"category": "Skin & wounds",
"searchTerms": [
"itching",
"rash",
"skin irritation",
"itchy skin",
"minor skin irritations",
"eczema"
]
},
{
slug: 'acne',
label: 'Acne',
category: 'Skin & wounds',
searchTerms: ['acne', 'pimples', 'blackheads', 'acne treatment'],
},
// ── Eyes, ears & mouth ──────────────────────────────────────────────────
{
slug: 'eye-irritation',
label: 'Eye irritation & dryness',
category: 'Eyes, ears & mouth',
searchTerms: [
'eye irritation',
'dry eyes',
'red eyes',
'eye redness',
'itchy eyes',
'lubricant eye',
],
"slug": "poison-ivy",
"label": "Poison ivy & plant rashes",
"category": "Skin & wounds",
"searchTerms": [
"poison ivy",
"poison oak",
"poison sumac",
"rashes due to poison ivy"
]
},
{
slug: 'earache',
label: 'Earache & ear wax',
category: 'Eyes, ears & mouth',
searchTerms: ['earache', 'ear wax', 'earwax removal', 'ear pain', 'swimmers ear'],
"slug": "fungal-infection",
"label": "Athletes foot & ringworm",
"category": "Skin & wounds",
"searchTerms": [
"athletes foot",
"ringworm",
"jock itch",
"antifungal",
"fungal infection",
"tinea"
]
},
{
slug: 'canker-sores',
label: 'Canker & cold sores',
category: 'Eyes, ears & mouth',
searchTerms: ['canker sores', 'cold sores', 'mouth sores', 'fever blisters', 'oral pain'],
"slug": "dry-skin",
"label": "Dry & chapped skin",
"category": "Skin & wounds",
"searchTerms": [
"dry skin",
"chapped skin",
"chapped lips",
"skin protectant",
"cracked skin"
]
},
{
slug: 'toothache',
label: 'Toothache',
category: 'Eyes, ears & mouth',
searchTerms: ['toothache', 'tooth pain', 'dental pain', 'oral analgesic'],
},
// ── Sleep, stress & general ─────────────────────────────────────────────
{
slug: 'sleeplessness',
label: 'Sleeplessness',
category: 'Sleep, stress & general',
searchTerms: [
'sleeplessness',
'insomnia',
'sleep aid',
'difficulty falling asleep',
'nighttime',
],
"slug": "acne",
"label": "Acne",
"category": "Skin & wounds",
"searchTerms": [
"acne",
"pimples",
"blackheads",
"acne treatment"
]
},
{
slug: 'dehydration',
label: 'Dehydration',
category: 'Sleep, stress & general',
searchTerms: [
'dehydration',
'oral rehydration',
'electrolyte',
'fluid loss',
'replaces electrolytes',
],
"slug": "eye-irritation",
"label": "Eye irritation & dryness",
"category": "Eyes, ears & mouth",
"searchTerms": [
"eye irritation",
"dry eyes",
"red eyes",
"eye redness",
"itchy eyes",
"lubricant eye"
]
},
{
slug: 'hemorrhoids',
label: 'Hemorrhoids',
category: 'Sleep, stress & general',
searchTerms: ['hemorrhoids', 'hemorrhoidal', 'anal itching', 'rectal'],
},
// ── Infections (OTC-treatable) ──────────────────────────────────────────
{
slug: 'yeast-infection',
label: 'Vaginal yeast infection',
category: 'Infections (OTC-treatable)',
searchTerms: [
'vaginal yeast infection',
'yeast infection',
'vaginal antifungal',
'candidiasis',
],
"slug": "earache",
"label": "Earache & ear wax",
"category": "Eyes, ears & mouth",
"searchTerms": [
"earache",
"ear wax",
"earwax removal",
"ear pain",
"swimmers ear"
]
},
{
slug: 'pinworm',
label: 'Pinworm',
category: 'Infections (OTC-treatable)',
searchTerms: ['pinworm', 'pinworm infection', 'pinworm treatment'],
"slug": "canker-sores",
"label": "Canker & cold sores",
"category": "Eyes, ears & mouth",
"searchTerms": [
"canker sores",
"cold sores",
"mouth sores",
"fever blisters",
"oral pain"
]
},
{
slug: 'cold-sore-lip',
label: 'Chapped & sun-protected lips',
category: 'Sleep, stress & general',
searchTerms: ['lip protectant', 'chapped lips', 'sunburn protection lips', 'lip balm'],
"slug": "toothache",
"label": "Toothache",
"category": "Eyes, ears & mouth",
"searchTerms": [
"toothache",
"tooth pain",
"dental pain",
"oral analgesic"
]
},
{
slug: 'eye-allergy',
label: 'Eye allergies',
category: 'Eyes, ears & mouth',
searchTerms: [
'eye allergy',
'itchy eyes due to allergies',
'ocular itching',
'allergic conjunctivitis',
],
"slug": "sleeplessness",
"label": "Sleeplessness",
"category": "Sleep, stress & general",
"searchTerms": [
"sleeplessness",
"insomnia",
"sleep aid",
"difficulty falling asleep",
"nighttime"
]
},
],
{
"slug": "dehydration",
"label": "Dehydration",
"category": "Sleep, stress & general",
"searchTerms": [
"dehydration",
"oral rehydration",
"electrolyte",
"fluid loss",
"replaces electrolytes"
]
},
{
"slug": "hemorrhoids",
"label": "Hemorrhoids",
"category": "Sleep, stress & general",
"searchTerms": [
"hemorrhoids",
"hemorrhoidal",
"anal itching",
"rectal"
]
},
{
"slug": "yeast-infection",
"label": "Vaginal yeast infection",
"category": "Infections (OTC-treatable)",
"searchTerms": [
"vaginal yeast infection",
"yeast infection",
"vaginal antifungal",
"candidiasis"
]
},
{
"slug": "pinworm",
"label": "Pinworm",
"category": "Infections (OTC-treatable)",
"searchTerms": [
"pinworm",
"pinworm infection",
"pinworm treatment"
]
},
{
"slug": "cold-sore-lip",
"label": "Chapped & sun-protected lips",
"category": "Sleep, stress & general",
"searchTerms": [
"lip protectant",
"chapped lips",
"sunburn protection lips",
"lip balm"
]
},
{
"slug": "eye-allergy",
"label": "Eye allergies",
"category": "Eyes, ears & mouth",
"searchTerms": [
"eye allergy",
"itchy eyes due to allergies",
"ocular itching",
"allergic conjunctivitis"
]
}
]
}

View File

@ -1,14 +1,13 @@
/* eslint-disable */
/**
* Non-herbal home-care / self-care measures (Phase 2b, runtime source of truth).
* GENERATED FILE DO NOT EDIT.
*
* Same pattern as natural_remedies.ts / conditions.ts: the repo-root
* collections/home_remedies.json is the browseable mirror, this compiled module
* is what the running app reads (the image ships only compiled admin/build/).
* Keep the two in sync. Sources are US-government public-domain pages (CDC,
* NIH/NLM MedlinePlus summaries, FDA consumer updates) each entry carries its
* exact sourceUrl.
* Single source of truth: collections/home_remedies.json
* Regenerate after editing the JSON: `npm run gen:curated-data` (from admin/).
* curated_data_sync.standalone.ts fails CI if this file drifts from the JSON.
*
* Non-herbal home-care / self-care measures from US-government public-domain pages (CDC, NIH/NLM MedlinePlus, FDA); each entry carries its own sourceUrl.
*/
import type { NaturalRemediesFile } from '../../types/conditions.js'
export const HOME_REMEDIES_FILE: NaturalRemediesFile = {

View File

@ -1,26 +1,13 @@
/* eslint-disable */
/**
* "When to use what" curated natural-remedies data (Phase 2, runtime source of truth).
* GENERATED FILE DO NOT EDIT.
*
* Hand-curated subset of NCCIH "Herbs at a Glance" (nccih.nih.gov) mapped to our
* 36-condition spine. Source is US government / public domain; reproduction
* encouraged with credit (credit appears in the NaturalRemediesFile.source field
* and in the UI section caveat).
* Single source of truth: collections/natural_remedies.json
* Regenerate after editing the JSON: `npm run gen:curated-data` (from admin/).
* curated_data_sync.standalone.ts fails CI if this file drifts from the JSON.
*
* WHY a TS constant (not a JSON file read at runtime):
* The repo-root `collections/natural_remedies.json` mirror exists for human
* discoverability and parity with the kiwix-categories / conditions convention,
* but it is NOT what the running app reads. The Dockerfile copies only `admin/`
* into the image (`ADD admin/ ./`) and ships only the compiled `build/` output,
* so a repo-root JSON file never reaches the container filesystem. Bundling the
* remedies as a compiled module guarantees they are always present at runtime
* with no path-resolution fragility.
*
* Keep this file and `collections/natural_remedies.json` in sync same `version`,
* same remedy slugs, same remedy count. `parseNaturalRemediesFile` (admin/util/conditions.ts)
* validates the shape so a hand-edit that breaks an entry degrades to "skip that
* entry" rather than crashing the page. The standalone test asserts sync.
* Hand-curated subset of NCCIH "Herbs at a Glance" (nccih.nih.gov), mapped to the condition spine. US-government / public domain; attribution lives in the file `source` field and the UI caveat.
*/
import type { NaturalRemediesFile } from '../../types/conditions.js'
export const NATURAL_REMEDIES_FILE: NaturalRemediesFile = {

View File

@ -12,6 +12,7 @@
"test": "node ace test",
"lint": "eslint .",
"format": "prettier --write .",
"gen:curated-data": "node --experimental-strip-types scripts/generate_curated_data.ts && prettier --write app/data/conditions.ts app/data/natural_remedies.ts app/data/home_remedies.ts",
"typecheck": "tsc --noEmit",
"work:downloads": "node ace queue:work --queue=downloads",
"work:model-downloads": "node ace queue:work --queue=model-downloads",

View File

@ -0,0 +1,79 @@
/**
* Generates the curated drug-reference data modules
* (`app/data/{conditions,natural_remedies,home_remedies}.ts`) from the
* single-source JSON in the repo-root `collections/` directory.
*
* npm run gen:curated-data (from admin/)
*
* The JSON files are the ONLY thing a human edits. The generated `.ts` modules
* keep the data compiled into the image (no runtime file read, no path
* fragility), and `tests/standalone/curated_data_sync.standalone.ts` fails CI if
* a generated module ever drifts from its JSON. This removes the old burden of
* hand-keeping the `.json` mirror and the `.ts` constant in sync
* (Crosstalk-Solutions/project-nomad#1040 follow-up).
*/
import { readFileSync, writeFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, resolve } from 'node:path'
const here = dirname(fileURLToPath(import.meta.url)) // admin/scripts
const collectionsDir = resolve(here, '../../collections')
const dataDir = resolve(here, '../app/data')
interface Spec {
name: string
exportName: string
typeName: string
provenance: string
}
const SPECS: Spec[] = [
{
name: 'conditions',
exportName: 'CONDITIONS_FILE',
typeName: 'ConditionsFile',
provenance:
'Curated "when to use what" condition spine (Phase 1). US-government / public-domain sourcing.',
},
{
name: 'natural_remedies',
exportName: 'NATURAL_REMEDIES_FILE',
typeName: 'NaturalRemediesFile',
provenance:
'Hand-curated subset of NCCIH "Herbs at a Glance" (nccih.nih.gov), mapped to the condition spine. ' +
'US-government / public domain; attribution lives in the file `source` field and the UI caveat.',
},
{
name: 'home_remedies',
exportName: 'HOME_REMEDIES_FILE',
typeName: 'NaturalRemediesFile',
provenance:
'Non-herbal home-care / self-care measures from US-government public-domain pages ' +
'(CDC, NIH/NLM MedlinePlus, FDA); each entry carries its own sourceUrl.',
},
]
for (const spec of SPECS) {
const json = JSON.parse(readFileSync(resolve(collectionsDir, `${spec.name}.json`), 'utf8'))
const banner =
`/* eslint-disable */\n` +
`/**\n` +
` * GENERATED FILE — DO NOT EDIT.\n` +
` *\n` +
` * Single source of truth: collections/${spec.name}.json\n` +
` * Regenerate after editing the JSON: \`npm run gen:curated-data\` (from admin/).\n` +
` * curated_data_sync.standalone.ts fails CI if this file drifts from the JSON.\n` +
` *\n` +
` * ${spec.provenance}\n` +
` */\n`
const module =
`import type { ${spec.typeName} } from '../../types/conditions.js'\n\n` +
`export const ${spec.exportName}: ${spec.typeName} = ${JSON.stringify(json, null, 2)}\n`
writeFileSync(resolve(dataDir, `${spec.name}.ts`), banner + module)
console.log(`generated app/data/${spec.name}.ts ← collections/${spec.name}.json`)
}
console.log('\nDone. Run `npm run format` is applied by the gen script; commit both the JSON and the generated .ts.')

View File

@ -0,0 +1,46 @@
/**
* Drift guard for the curated drug-reference data (upstream #1040 follow-up).
*
* node --experimental-strip-types tests/standalone/curated_data_sync.standalone.ts
*
* The `collections/*.json` files are the single source of truth; the matching
* `app/data/*.ts` modules are generated from them (`npm run gen:curated-data`).
* This test fails if a generated module ever drifts from its JSON i.e. if
* someone hand-edited the `.ts` instead of the JSON, or forgot to regenerate.
* A failure means: edit the JSON, then run `npm run gen:curated-data`.
*/
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, resolve } from 'node:path'
import { CONDITIONS_FILE } from '../../app/data/conditions.ts'
import { NATURAL_REMEDIES_FILE } from '../../app/data/natural_remedies.ts'
import { HOME_REMEDIES_FILE } from '../../app/data/home_remedies.ts'
const collectionsDir = resolve(dirname(fileURLToPath(import.meta.url)), '../../../collections')
let passed = 0
function check(name: string, fn: () => void) {
fn()
passed++
console.log(` ok - ${name}`)
}
const CASES: Array<[string, unknown]> = [
['conditions', CONDITIONS_FILE],
['natural_remedies', NATURAL_REMEDIES_FILE],
['home_remedies', HOME_REMEDIES_FILE],
]
for (const [name, generated] of CASES) {
check(`app/data/${name}.ts is in sync with collections/${name}.json`, () => {
const json = JSON.parse(readFileSync(resolve(collectionsDir, `${name}.json`), 'utf8'))
assert.deepEqual(
generated,
json,
`app/data/${name}.ts drifted from collections/${name}.json — edit the JSON, then run \`npm run gen:curated-data\``
)
})
}
console.log(`\n${passed} passed`)