From cda8f740afc144c73b02470aa828f355a436c232 Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 23 Jun 2026 07:04:57 -0500 Subject: [PATCH] =?UTF-8?q?fix(drug-reference):=20address=20review=20?= =?UTF-8?q?=E2=80=94=20defer=20FULLTEXT,=20fallback=20remedies=20prop,=20s?= =?UTF-8?q?trip=20fork=20refs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - migration: wrap the FULLTEXT ALTER in this.defer so it runs after the deferred createTable (was silently swallowed, index never created) - controller: add remedies:[] to the index() error fallback (required prop) - strip fork-internal issue/spec references from ported comments - tsconfig: exclude tests/standalone (node --experimental-strip-types only) - correct the varchar(768) byte-math comment; extend remedy-spine test --- .../controllers/drug_reference_controller.ts | 1 + admin/app/data/conditions.ts | 8 ++-- admin/app/data/home_remedies.ts | 2 +- admin/app/data/natural_remedies.ts | 2 +- admin/app/services/condition_service.ts | 6 +-- .../1778600000004_create_drug_labels_table.ts | 37 ++++++++++++------- ...00000006_add_indications_fulltext_index.ts | 2 +- .../components/conditions/SafetyBanner.tsx | 6 +-- admin/inertia/pages/drug-reference/index.tsx | 2 +- admin/inertia/pages/drug-reference/show.tsx | 2 +- .../tests/standalone/conditions.standalone.ts | 4 +- .../standalone/natural_remedies.standalone.ts | 2 +- admin/tsconfig.json | 2 +- admin/types/conditions.ts | 7 ++-- admin/util/conditions.ts | 4 +- 15 files changed, 49 insertions(+), 38 deletions(-) diff --git a/admin/app/controllers/drug_reference_controller.ts b/admin/app/controllers/drug_reference_controller.ts index 739204d..c0906eb 100644 --- a/admin/app/controllers/drug_reference_controller.ts +++ b/admin/app/controllers/drug_reference_controller.ts @@ -49,6 +49,7 @@ export default class DrugReferenceController { ingestStatus: null, rowCount: 0, conditions: [], + remedies: [], }) } } diff --git a/admin/app/data/conditions.ts b/admin/app/data/conditions.ts index a5575be..0ab8cb7 100644 --- a/admin/app/data/conditions.ts +++ b/admin/app/data/conditions.ts @@ -1,9 +1,9 @@ /** * "When to use what" — curated condition spine (Phase 1, runtime source of truth). * - * A bounded, hand-curated list of common first-aid / emergency situations — the - * "small booklet" the upstream #664 requester described. Each entry's - * `searchTerms` drive the FULLTEXT search over `drug_labels.indications`. + * 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`. * * WHY a TS constant (not a JSON file read at runtime): * The repo-root `collections/conditions.json` mirror exists for parity with @@ -12,7 +12,7 @@ * 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. The spec explicitly allows "JSON/TS constant". + * 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 diff --git a/admin/app/data/home_remedies.ts b/admin/app/data/home_remedies.ts index 10f593a..9548d20 100644 --- a/admin/app/data/home_remedies.ts +++ b/admin/app/data/home_remedies.ts @@ -6,7 +6,7 @@ * 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. See GitHub issue #23. + * exact sourceUrl. */ import type { NaturalRemediesFile } from '../../types/conditions.js' diff --git a/admin/app/data/natural_remedies.ts b/admin/app/data/natural_remedies.ts index 1ffd863..d64c516 100644 --- a/admin/app/data/natural_remedies.ts +++ b/admin/app/data/natural_remedies.ts @@ -13,7 +13,7 @@ * 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. The spec explicitly allows "JSON/TS constant". + * 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) diff --git a/admin/app/services/condition_service.ts b/admin/app/services/condition_service.ts index f1fbc06..a4d5343 100644 --- a/admin/app/services/condition_service.ts +++ b/admin/app/services/condition_service.ts @@ -41,9 +41,9 @@ import type { DrugSearchResult } from '../../types/drug_reference.js' /** * Module-level merged remedies corpus (fail-soft): the NCCIH herbs plus the - * non-herbal home-care measures (CDC/NIH/FDA, issue #23), each entry tagged - * with its kind so the UI can badge them apart. Slugs are disjoint between the - * two files (validated at curation time). + * non-herbal home-care measures (CDC/NIH/FDA), each entry tagged with its kind + * so the UI can badge them apart. Slugs are disjoint between the two files + * (validated at curation time). */ const HERB_FILE = parseNaturalRemediesFile(NATURAL_REMEDIES_FILE) const HOME_FILE = parseNaturalRemediesFile(HOME_REMEDIES_FILE) diff --git a/admin/database/migrations/1778600000004_create_drug_labels_table.ts b/admin/database/migrations/1778600000004_create_drug_labels_table.ts index 956f06d..eca2f03 100644 --- a/admin/database/migrations/1778600000004_create_drug_labels_table.ts +++ b/admin/database/migrations/1778600000004_create_drug_labels_table.ts @@ -9,12 +9,13 @@ import { BaseSchema } from '@adonisjs/lucid/schema' * the ingest refreshes existing rows in place; no manual purge needed. * * Section text columns use mediumtext (up to 16 MB) so no openFDA section is - * truncated. The `searchable_name` varchar(768) stays within InnoDB's index - * key-length budget under utf8mb4 (191 chars × 4 bytes = 764 < 767 limit for - * a single column; 768 bytes here fits because MySQL counts bytes for the - * key-length limit when the column is declared as varchar, not character count - * for the prefix approach). If a tighter budget is needed in a utf8mb4_bin - * collation, use varchar(191) — but the utf8mb4 default collation is fine. + * truncated. The `searchable_name` varchar(768) is indexed (idx_drug_labels_searchable_name). + * Under utf8mb4 that column is 768 × 4 = 3072 bytes, which is exactly the InnoDB + * index-prefix limit for the DYNAMIC / COMPRESSED row formats (DYNAMIC is the + * MySQL 8.0 default). It would overflow the 767-byte limit of the older + * REDUNDANT / COMPACT row formats, so this depends on the 8.0 default row format. + * If a deployment forces an older row format, drop the column to varchar(191) + * (191 × 4 = 764 ≤ 767) to stay within the 767-byte budget. * * The FULLTEXT index is created in a guarded try/catch so a non-InnoDB engine * or an older MySQL version that doesn't support FULLTEXT doesn't break the @@ -49,7 +50,8 @@ export default class extends BaseSchema { table.string('product_type', 32).nullable() // Normalized brand+generic blob — computed once at ingest, never on read. - // 768 chars stays within InnoDB's utf8mb4 index key-length budget. + // 768 chars × 4 bytes (utf8mb4) = 3072 = the InnoDB DYNAMIC/COMPRESSED + // index-prefix limit (see header note). table.string('searchable_name', 768).nullable() // Section text — mediumtext so even the longest FDA label bodies are stored @@ -95,13 +97,20 @@ export default class extends BaseSchema { // name+indications index (search-by-what-it-treats) is deferred: FULLTEXT // can't take a prefix length, and indexing the full mediumtext body adds heavy // index weight v1 doesn't use. - try { - await this.db.rawQuery( - `ALTER TABLE drug_labels ADD FULLTEXT INDEX ft_drug_labels_name (searchable_name)` - ) - } catch { - // Non-InnoDB or FULLTEXT unsupported — search falls back to LIKE. - } + // + // Deferred so it runs AFTER createTable executes — Lucid's schema builder + // is deferred, so a bare ALTER here would hit a not-yet-created table. The + // this.defer(db => …) pattern (see 1775100000001_create_custom_library_sources_table.ts) + // queues it to run on the live connection once the table exists. + this.defer(async (db) => { + try { + await db.rawQuery( + `ALTER TABLE drug_labels ADD FULLTEXT INDEX ft_drug_labels_name (searchable_name)` + ) + } catch { + // Non-InnoDB or FULLTEXT unsupported — search falls back to LIKE. + } + }) } async down() { diff --git a/admin/database/migrations/1778600000006_add_indications_fulltext_index.ts b/admin/database/migrations/1778600000006_add_indications_fulltext_index.ts index 6f612bb..f3e5971 100644 --- a/admin/database/migrations/1778600000006_add_indications_fulltext_index.ts +++ b/admin/database/migrations/1778600000006_add_indications_fulltext_index.ts @@ -10,7 +10,7 @@ import { BaseSchema } from '@adonisjs/lucid/schema' * Design notes: * - FULLTEXT indexes cannot take a column prefix length, so the full * mediumtext body of `indications` is indexed. On ~259k rows this adds - * meaningful index weight (tracked in issue #11). + * meaningful index weight. * - The guard mirrors the existing ft_drug_labels_name guard in migration * 1778600000004: a non-InnoDB engine or a MySQL version without FULLTEXT * support must not block the migration runner. The indication-search path diff --git a/admin/inertia/components/conditions/SafetyBanner.tsx b/admin/inertia/components/conditions/SafetyBanner.tsx index 2b07f22..8ff2af9 100644 --- a/admin/inertia/components/conditions/SafetyBanner.tsx +++ b/admin/inertia/components/conditions/SafetyBanner.tsx @@ -4,9 +4,9 @@ import { IconAlertTriangle } from '@tabler/icons-react' * "When to use what" — top-of-page safety banner. * * A prominent amber callout that renders at the TOP of both the condition index - * and detail pages. This is a hard ship requirement (per the design spec), not a - * footnote: results are FDA label-indication matches, NOT recommendations, NOT - * an FDA endorsement, and NOT a drug-interaction checker. + * and detail pages: results are FDA label-indication matches, NOT + * recommendations, NOT an FDA endorsement, and NOT a drug-interaction checker. + * It leads the page (not a footnote) so the caveat is read before any result. */ export default function SafetyBanner() { return ( diff --git a/admin/inertia/pages/drug-reference/index.tsx b/admin/inertia/pages/drug-reference/index.tsx index 2b2a032..2ab9ae4 100644 --- a/admin/inertia/pages/drug-reference/index.tsx +++ b/admin/inertia/pages/drug-reference/index.tsx @@ -112,7 +112,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 = [] }: PageProps) { const [query, setQuery] = useState('') const [productType, setProductType] = useState(null) const [route, setRoute] = useState(null) diff --git a/admin/inertia/pages/drug-reference/show.tsx b/admin/inertia/pages/drug-reference/show.tsx index d59bc0c..8e07e33 100644 --- a/admin/inertia/pages/drug-reference/show.tsx +++ b/admin/inertia/pages/drug-reference/show.tsx @@ -156,7 +156,7 @@ export default function DrugReferenceShow({ label, situations = [] }: PageProps) )} diff --git a/admin/tests/standalone/conditions.standalone.ts b/admin/tests/standalone/conditions.standalone.ts index d5dd0f9..ef13035 100644 --- a/admin/tests/standalone/conditions.standalone.ts +++ b/admin/tests/standalone/conditions.standalone.ts @@ -8,7 +8,7 @@ * node --experimental-strip-types tests/standalone/conditions.standalone.ts * * Also smoke-checks the shipped curated spine (admin/app/data/conditions.ts) - * parses cleanly and stays in the 30–50 range the spec calls for. + * parses cleanly and stays a bounded curated list of ~30–50 conditions. */ import assert from 'node:assert/strict' import { @@ -276,7 +276,7 @@ check('shipped CONDITIONS_FILE parses cleanly with no dropped entries', () => { assert.equal(parsed.conditions.length, CONDITIONS_FILE.conditions.length) }) -check('shipped spine holds 30–50 curated conditions (spec range)', () => { +check('shipped spine holds a bounded curated list of ~30–50 conditions', () => { const n = CONDITIONS_FILE.conditions.length assert.ok(n >= 30 && n <= 50, `expected 30–50 conditions, got ${n}`) }) diff --git a/admin/tests/standalone/natural_remedies.standalone.ts b/admin/tests/standalone/natural_remedies.standalone.ts index 5e7fdf0..0e3f051 100644 --- a/admin/tests/standalone/natural_remedies.standalone.ts +++ b/admin/tests/standalone/natural_remedies.standalone.ts @@ -311,7 +311,7 @@ check('every remedy condition slug exists in the conditions spine', () => { ) }) -check('shipped NATURAL_REMEDIES_FILE has 18 remedies (spec count)', () => { +check('shipped NATURAL_REMEDIES_FILE has the 18 curated remedies', () => { const n = NATURAL_REMEDIES_FILE.remedies.length assert.equal(n, 18, `expected 18 remedies, got ${n}`) }) diff --git a/admin/tsconfig.json b/admin/tsconfig.json index bb4649e..b16e194 100644 --- a/admin/tsconfig.json +++ b/admin/tsconfig.json @@ -4,5 +4,5 @@ "rootDir": "./", "outDir": "./build" }, - "exclude": ["./inertia/**/*", "node_modules", "build"] + "exclude": ["./inertia/**/*", "tests/standalone/**/*", "node_modules", "build"] } diff --git a/admin/types/conditions.ts b/admin/types/conditions.ts index f006c83..9cd0242 100644 --- a/admin/types/conditions.ts +++ b/admin/types/conditions.ts @@ -4,7 +4,7 @@ * A condition (situation) is a curated first-aid / emergency scenario the user * browses or searches. Each carries `searchTerms` (synonyms) that drive the * FULLTEXT search over `drug_labels.indications` — the same machinery the - * Drug Reference indication search (#11) uses. + * indication FULLTEXT search uses. * * Phase 1 maps conditions → OTC drugs. * Phase 2 adds natural remedies from NCCIH, resolved against the same condition @@ -78,8 +78,9 @@ export interface NaturalRemedy { /** * Which curated corpus the entry came from. 'herb' = NCCIH herbal fact * sheets; 'self-care' = non-herbal home-care measures from CDC/NIH/FDA pages - * (issue #23). Assigned at merge time in condition_service — the JSON files - * don't carry it. Optional so older data parses; absent means 'herb'. + * (the non-herbal home-care entries). Assigned at merge time in + * condition_service — the JSON files don't carry it. Optional so older data + * parses; absent means 'herb'. */ kind?: 'herb' | 'self-care' } diff --git a/admin/util/conditions.ts b/admin/util/conditions.ts index 364cd1e..106c27c 100644 --- a/admin/util/conditions.ts +++ b/admin/util/conditions.ts @@ -165,8 +165,8 @@ export function situationsForIndications( * searchTerms. * * - Multi-word terms are wrapped in double quotes so they match as a phrase - * ("sore throat") rather than as two loose tokens — this tightens precision, - * the trade-off the spec calls out for relevance-vs-precision. + * ("sore throat") rather than as two loose tokens — this trades a little + * recall for precision on multi-word conditions. * - Single-word terms are passed through bare. * - Internal double-quotes inside a term are stripped (they would break the * phrase quoting).