@cyanheads/medical-codes-mcp-server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/AGENTS.md +375 -0
  2. package/CLAUDE.md +375 -0
  3. package/Dockerfile +124 -0
  4. package/LICENSE +201 -0
  5. package/README.md +328 -0
  6. package/changelog/0.1.x/0.1.0.md +24 -0
  7. package/changelog/template.md +127 -0
  8. package/data/medical-codes.db +0 -0
  9. package/dist/config/server-config.d.ts +23 -0
  10. package/dist/config/server-config.d.ts.map +1 -0
  11. package/dist/config/server-config.js +51 -0
  12. package/dist/config/server-config.js.map +1 -0
  13. package/dist/index.d.ts +9 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +54 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/mcp-server/tools/definitions/_render.d.ts +26 -0
  18. package/dist/mcp-server/tools/definitions/_render.d.ts.map +1 -0
  19. package/dist/mcp-server/tools/definitions/_render.js +45 -0
  20. package/dist/mcp-server/tools/definitions/_render.js.map +1 -0
  21. package/dist/mcp-server/tools/definitions/browse-hierarchy.tool.d.ts +51 -0
  22. package/dist/mcp-server/tools/definitions/browse-hierarchy.tool.d.ts.map +1 -0
  23. package/dist/mcp-server/tools/definitions/browse-hierarchy.tool.js +131 -0
  24. package/dist/mcp-server/tools/definitions/browse-hierarchy.tool.js.map +1 -0
  25. package/dist/mcp-server/tools/definitions/check-code.tool.d.ts +42 -0
  26. package/dist/mcp-server/tools/definitions/check-code.tool.d.ts.map +1 -0
  27. package/dist/mcp-server/tools/definitions/check-code.tool.js +91 -0
  28. package/dist/mcp-server/tools/definitions/check-code.tool.js.map +1 -0
  29. package/dist/mcp-server/tools/definitions/get-code.tool.d.ts +52 -0
  30. package/dist/mcp-server/tools/definitions/get-code.tool.d.ts.map +1 -0
  31. package/dist/mcp-server/tools/definitions/get-code.tool.js +165 -0
  32. package/dist/mcp-server/tools/definitions/get-code.tool.js.map +1 -0
  33. package/dist/mcp-server/tools/definitions/list-systems.tool.d.ts +21 -0
  34. package/dist/mcp-server/tools/definitions/list-systems.tool.d.ts.map +1 -0
  35. package/dist/mcp-server/tools/definitions/list-systems.tool.js +81 -0
  36. package/dist/mcp-server/tools/definitions/list-systems.tool.js.map +1 -0
  37. package/dist/mcp-server/tools/definitions/map-codes.tool.d.ts +55 -0
  38. package/dist/mcp-server/tools/definitions/map-codes.tool.d.ts.map +1 -0
  39. package/dist/mcp-server/tools/definitions/map-codes.tool.js +132 -0
  40. package/dist/mcp-server/tools/definitions/map-codes.tool.js.map +1 -0
  41. package/dist/mcp-server/tools/definitions/search-codes.tool.d.ts +48 -0
  42. package/dist/mcp-server/tools/definitions/search-codes.tool.d.ts.map +1 -0
  43. package/dist/mcp-server/tools/definitions/search-codes.tool.js +133 -0
  44. package/dist/mcp-server/tools/definitions/search-codes.tool.js.map +1 -0
  45. package/dist/services/code-index/code-index-service.d.ts +185 -0
  46. package/dist/services/code-index/code-index-service.d.ts.map +1 -0
  47. package/dist/services/code-index/code-index-service.js +530 -0
  48. package/dist/services/code-index/code-index-service.js.map +1 -0
  49. package/dist/services/code-index/detect.d.ts +23 -0
  50. package/dist/services/code-index/detect.d.ts.map +1 -0
  51. package/dist/services/code-index/detect.js +58 -0
  52. package/dist/services/code-index/detect.js.map +1 -0
  53. package/dist/services/code-index/schema.d.ts +55 -0
  54. package/dist/services/code-index/schema.d.ts.map +1 -0
  55. package/dist/services/code-index/schema.js +129 -0
  56. package/dist/services/code-index/schema.js.map +1 -0
  57. package/dist/services/code-index/types.d.ts +71 -0
  58. package/dist/services/code-index/types.d.ts.map +1 -0
  59. package/dist/services/code-index/types.js +24 -0
  60. package/dist/services/code-index/types.js.map +1 -0
  61. package/package.json +92 -0
  62. package/server.json +125 -0
@@ -0,0 +1,530 @@
1
+ /**
2
+ * @fileoverview The code-index service — the single source of truth for the
3
+ * server. Opens the bundled SQLite + FTS5 database read-only at startup and
4
+ * exposes typed query methods the six tools compose: decode, search, validate,
5
+ * crosswalk, and hierarchy-browse. No network, no tenant state — the corpus is
6
+ * global, read-only, and built at package-build time.
7
+ * @module services/code-index/code-index-service
8
+ */
9
+ import { existsSync } from 'node:fs';
10
+ import { dirname, join } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { internalError } from '@cyanheads/mcp-ts-core/errors';
13
+ import { logger, requestContextService, runtimeCaps } from '@cyanheads/mcp-ts-core/utils';
14
+ import { getServerConfig } from '../../config/server-config.js';
15
+ import { detectSystems } from './detect.js';
16
+ import { displayCode, icd10cmParent, storageCode } from './schema.js';
17
+ import { DRUG_DIRECTIONS, SYSTEM_IDS, } from './types.js';
18
+ /**
19
+ * Open the bundled DB read-only with whichever driver the runtime provides, and
20
+ * normalize it to {@link SqliteDb}. Bun uses the built-in `bun:sqlite`; Node and
21
+ * the Vitest worker (which runs as Node) use the `better-sqlite3` optional dep.
22
+ * Both are loaded via variable-specifier dynamic import so the project typechecks
23
+ * without `bun-types` and builds without `better-sqlite3` resolved at compile time.
24
+ */
25
+ async function openDriver(dbPath) {
26
+ if (runtimeCaps.isBun) {
27
+ const BUN_SQLITE = 'bun:sqlite';
28
+ const { Database } = (await import(BUN_SQLITE));
29
+ const db = new Database(dbPath, { readonly: true });
30
+ return { query: (sql) => db.query(sql), close: () => db.close() };
31
+ }
32
+ const BETTER_SQLITE3 = 'better-sqlite3';
33
+ let mod;
34
+ try {
35
+ mod = (await import(BETTER_SQLITE3));
36
+ }
37
+ catch (err) {
38
+ throw internalError('medical-codes-mcp-server needs a SQLite driver: the built-in `bun:sqlite` (run with Bun — the published Docker image and npm bin both do) or the `better-sqlite3` optional dependency for Node. Neither was found.', { dbPath }, { cause: err });
39
+ }
40
+ const db = new mod.default(dbPath, { readonly: true, fileMustExist: true });
41
+ return { query: (sql) => db.prepare(sql), close: () => db.close() };
42
+ }
43
+ const FALLBACK_DB_FILENAME = 'medical-codes.db';
44
+ /**
45
+ * Resolve the bundled DB path. An explicit `MEDCODE_DB_PATH` wins; otherwise the
46
+ * packaged `data/medical-codes.db`, resolved relative to this module's location
47
+ * so it works from both `src/` (tests, bun) and `dist/` (production build).
48
+ */
49
+ function resolveDbPath() {
50
+ const configured = getServerConfig().dbPath;
51
+ if (configured)
52
+ return configured;
53
+ const here = dirname(fileURLToPath(import.meta.url));
54
+ // here is …/src/services/code-index or …/dist/services/code-index → up 3 to root.
55
+ return join(here, '..', '..', '..', 'data', FALLBACK_DB_FILENAME);
56
+ }
57
+ /**
58
+ * Read-only handle over the bundled medical-codes index. Constructed once in
59
+ * `setup()`. All query methods are synchronous SQLite reads — fast enough to
60
+ * call inline from handlers.
61
+ */
62
+ export class CodeIndexService {
63
+ db;
64
+ dbPath;
65
+ constructor(db, dbPath) {
66
+ this.db = db;
67
+ this.dbPath = dbPath;
68
+ }
69
+ /**
70
+ * Open the bundled DB read-only and assert it is a populated build. Fails fast
71
+ * (throws) on a missing file, an unreadable DB, or an empty `build_meta` — a
72
+ * broken bundle should crash loudly at startup, not serve empty results.
73
+ */
74
+ static async open() {
75
+ const dbPath = resolveDbPath();
76
+ if (!existsSync(dbPath)) {
77
+ throw internalError(`Bundled code index not found at ${dbPath}. The package ships data/medical-codes.db; set MEDCODE_DB_PATH to override, or rebuild it with \`bun run scripts/build-index.ts\`.`, { dbPath });
78
+ }
79
+ const db = await openDriver(dbPath);
80
+ const meta = db.query('SELECT COUNT(*) AS n FROM build_meta').get();
81
+ if (!meta || meta.n === 0) {
82
+ db.close();
83
+ throw internalError(`Bundled code index at ${dbPath} has no build_meta rows — the database is empty or corrupt.`, { dbPath });
84
+ }
85
+ logger.info('Code index opened', requestContextService.createRequestContext({
86
+ operation: 'CodeIndexOpen',
87
+ dbPath,
88
+ systems: meta.n,
89
+ }));
90
+ return new CodeIndexService(db, dbPath);
91
+ }
92
+ /** Map a raw `codes` row (snake_case, 0/1 ints) to a typed CodeRow. */
93
+ static toCodeRow(raw) {
94
+ return {
95
+ system: raw.system,
96
+ code: raw.code,
97
+ shortDesc: raw.short_desc ?? null,
98
+ longDesc: raw.long_desc ?? null,
99
+ billable: Number(raw.billable ?? 0),
100
+ header: Number(raw.header ?? 0),
101
+ chapter: raw.chapter ?? null,
102
+ parent: raw.parent ?? null,
103
+ effective: raw.effective ?? null,
104
+ terminated: raw.terminated ?? null,
105
+ };
106
+ }
107
+ /** Project a CodeRow into the display shape tools return. */
108
+ static decode(row) {
109
+ return {
110
+ system: row.system,
111
+ code: displayCode(row.system, row.code),
112
+ shortDescription: row.shortDesc,
113
+ description: row.longDesc ?? row.shortDesc,
114
+ billable: row.billable === 1,
115
+ header: row.header === 1,
116
+ chapter: row.chapter,
117
+ };
118
+ }
119
+ /** Lexically-plausible systems for a raw code (no DB hit). */
120
+ detectSystem(rawCode) {
121
+ return detectSystems(rawCode);
122
+ }
123
+ /** Fetch the raw row for a code in a specific system, or null. */
124
+ getRow(code, system) {
125
+ const raw = this.db
126
+ .query('SELECT * FROM codes WHERE system = ? AND code = ?')
127
+ .get(system, code);
128
+ return raw ? CodeIndexService.toCodeRow(raw) : null;
129
+ }
130
+ /**
131
+ * Find which systems actually contain a code string. Narrows by lexical shape
132
+ * first (cheap), then confirms membership in the DB. The lexical candidates
133
+ * may overlap (a letter+4-digit code is shaped like both ICD-10-CM and HCPCS);
134
+ * DB membership is the real disambiguator — a code present in exactly one
135
+ * system is unambiguous regardless of shape overlap.
136
+ */
137
+ resolveSystems(rawCode, explicit) {
138
+ const code = storageCode(rawCode);
139
+ const candidates = explicit ? [explicit] : detectSystems(rawCode);
140
+ return candidates.filter((sys) => this.getRow(code, sys) !== null);
141
+ }
142
+ /**
143
+ * Decode one code. Returns `{ kind: 'found' }` with the row, `'ambiguous'`
144
+ * with the colliding systems, or `'not_found'`. When `system` is given it is
145
+ * authoritative; otherwise membership across detected systems decides.
146
+ */
147
+ getByCode(rawCode, system) {
148
+ const code = storageCode(rawCode);
149
+ const present = this.resolveSystems(rawCode, system);
150
+ const [sys] = present;
151
+ if (!sys)
152
+ return { kind: 'not_found' };
153
+ if (present.length > 1)
154
+ return { kind: 'ambiguous', systems: present };
155
+ const row = this.getRow(code, sys);
156
+ return row ? { kind: 'found', row } : { kind: 'not_found' };
157
+ }
158
+ /** Immediate children of a code within its system (prefix hierarchy). */
159
+ childrenOf(system, parentCode) {
160
+ const rows = this.db
161
+ .query('SELECT * FROM codes WHERE system = ? AND parent = ? ORDER BY code LIMIT ?')
162
+ .all(system, parentCode, getServerConfig().maxResults);
163
+ return rows.map((r) => CodeIndexService.decode(CodeIndexService.toCodeRow(r)));
164
+ }
165
+ /** Decode with parent + immediate children attached. */
166
+ getByCodeWithHierarchy(row) {
167
+ const base = CodeIndexService.decode(row);
168
+ const parentStorage = row.system === 'ICD10CM' ? icd10cmParent(row.code) : row.parent;
169
+ const children = this.childrenOf(row.system, row.code);
170
+ return {
171
+ ...base,
172
+ parent: parentStorage ? displayCode(row.system, parentStorage) : null,
173
+ children,
174
+ };
175
+ }
176
+ /**
177
+ * Full-text search over code descriptions. Translates the user's text into a
178
+ * safe FTS5 MATCH expression (every token required, prefix-matched), applies
179
+ * the system/billable/chapter filters, and caps at `limit`.
180
+ */
181
+ searchFts(queryText, filters) {
182
+ const match = toFtsMatch(queryText);
183
+ if (!match)
184
+ return [];
185
+ const where = ['codes_fts MATCH ?'];
186
+ const params = [match];
187
+ if (filters.system) {
188
+ where.push('c.system = ?');
189
+ params.push(filters.system);
190
+ }
191
+ if (filters.billableOnly) {
192
+ where.push('c.billable = 1');
193
+ }
194
+ if (filters.chapter) {
195
+ where.push('c.chapter = ?');
196
+ params.push(filters.chapter);
197
+ }
198
+ params.push(filters.limit);
199
+ const rows = this.db
200
+ .query(`SELECT c.* FROM codes_fts f
201
+ JOIN codes c ON c.system = f.system AND c.code = f.code
202
+ WHERE ${where.join(' AND ')}
203
+ ORDER BY bm25(codes_fts) LIMIT ?`)
204
+ .all(...params);
205
+ return rows.map((r) => CodeIndexService.decode(CodeIndexService.toCodeRow(r)));
206
+ }
207
+ /**
208
+ * Validate a code's existence, currency, and billability. Existence vs.
209
+ * validity are split: a non-billable or terminated code is a *successful*
210
+ * status with a why-not, NOT a failure. Only a code absent from every detected
211
+ * system is `unknown`.
212
+ */
213
+ checkCode(rawCode, system) {
214
+ const code = storageCode(rawCode);
215
+ const present = this.resolveSystems(rawCode, system);
216
+ if (present.length > 1)
217
+ return { kind: 'ambiguous', systems: present };
218
+ if (present.length === 0) {
219
+ // Echo a best-guess system for the caller's orientation, if shape suggests one.
220
+ const detected = system ?? detectSystems(rawCode)[0];
221
+ return {
222
+ kind: 'resolved',
223
+ result: {
224
+ system: detected ?? 'ICD10CM',
225
+ code: rawCode.trim().toUpperCase(),
226
+ status: 'unknown',
227
+ whyNot: detected
228
+ ? `No ${detected} code matches "${rawCode.trim()}" in the bundled release.`
229
+ : `"${rawCode.trim()}" does not match the shape of any bundled code system.`,
230
+ },
231
+ };
232
+ }
233
+ // Exactly one system remains (zero and >1 handled above); resolveSystems only
234
+ // returns systems whose row exists, so getRow cannot be null here.
235
+ const [sys] = present;
236
+ const row = sys ? this.getRow(code, sys) : null;
237
+ if (!row)
238
+ return { kind: 'resolved', result: { system: sys ?? 'ICD10CM', code, status: 'unknown' } };
239
+ const display = displayCode(row.system, row.code);
240
+ if (row.terminated) {
241
+ return {
242
+ kind: 'resolved',
243
+ result: {
244
+ system: row.system,
245
+ code: display,
246
+ status: 'terminated',
247
+ whyNot: `Code terminated effective ${formatDate(row.terminated)}; no longer valid for current claims.`,
248
+ },
249
+ };
250
+ }
251
+ if (row.header === 1) {
252
+ return {
253
+ kind: 'resolved',
254
+ result: {
255
+ system: row.system,
256
+ code: display,
257
+ status: 'valid_header',
258
+ whyNot: 'Valid category/header, but not billable — submit a more specific child code instead.',
259
+ },
260
+ };
261
+ }
262
+ if (row.billable === 1) {
263
+ return {
264
+ kind: 'resolved',
265
+ result: { system: row.system, code: display, status: 'valid_billable' },
266
+ };
267
+ }
268
+ return {
269
+ kind: 'resolved',
270
+ result: {
271
+ system: row.system,
272
+ code: display,
273
+ status: 'valid_not_billable',
274
+ whyNot: 'Valid code, but not flagged billable in this release — verify a more specific code is not required before submitting.',
275
+ },
276
+ };
277
+ }
278
+ /**
279
+ * Crosswalk a value across systems or within a hierarchy. v1 supports the
280
+ * hierarchy directions (`parents`/`children`); the drug directions resolve
281
+ * against the RxNorm tables, which are empty until phase 2 — the tool guards
282
+ * for that before calling.
283
+ */
284
+ mapCode(from, direction, system) {
285
+ if (direction === 'parents' || direction === 'children') {
286
+ const present = this.resolveSystems(from, system);
287
+ const [sys] = present;
288
+ if (!sys)
289
+ return { kind: 'source_not_found' };
290
+ if (present.length > 1)
291
+ return { kind: 'ambiguous', systems: present };
292
+ const code = storageCode(from);
293
+ const row = this.getRow(code, sys);
294
+ if (!row)
295
+ return { kind: 'source_not_found' };
296
+ if (direction === 'children') {
297
+ const kids = this.childrenOf(sys, code);
298
+ return {
299
+ kind: 'ok',
300
+ resolvedSystem: sys,
301
+ hits: kids.map((k) => ({
302
+ source: sys,
303
+ system: sys,
304
+ value: k.code,
305
+ ...(k.description ? { description: k.description } : {}),
306
+ })),
307
+ };
308
+ }
309
+ // parents — single immediate parent for prefix systems; PCS has none.
310
+ const parentStorage = sys === 'ICD10CM' ? icd10cmParent(code) : row.parent;
311
+ if (!parentStorage)
312
+ return { kind: 'ok', resolvedSystem: sys, hits: [] };
313
+ const parentRow = this.getRow(parentStorage, sys);
314
+ if (!parentRow)
315
+ return { kind: 'ok', resolvedSystem: sys, hits: [] };
316
+ const parentDesc = parentRow.longDesc ?? parentRow.shortDesc;
317
+ return {
318
+ kind: 'ok',
319
+ resolvedSystem: sys,
320
+ hits: [
321
+ {
322
+ source: sys,
323
+ system: sys,
324
+ value: displayCode(sys, parentRow.code),
325
+ ...(parentDesc ? { description: parentDesc } : {}),
326
+ },
327
+ ],
328
+ };
329
+ }
330
+ // Drug directions — RxNorm-backed (phase 2). The tool short-circuits these
331
+ // with `direction_unavailable` when the RxNorm tables are unbuilt; this
332
+ // implementation is live the moment the tables are populated.
333
+ return this.mapDrug(from, direction);
334
+ }
335
+ /** Whether the RxNorm tables carry any rows (i.e. phase 2 is bundled). */
336
+ hasRxNorm() {
337
+ const row = this.db.query('SELECT COUNT(*) AS n FROM rxnorm_rel').get();
338
+ return (row?.n ?? 0) > 0;
339
+ }
340
+ /** RxNorm crosswalk resolution. */
341
+ mapDrug(from, direction) {
342
+ const value = from.trim();
343
+ switch (direction) {
344
+ case 'name_to_rxcui': {
345
+ // Escape LIKE wildcards in the user value so `%` / `_` / `\` match
346
+ // literally instead of acting as operators (an unescaped `%` would match
347
+ // every drug name). Bind the escaped term inside `%…%` and declare the
348
+ // escape char with ESCAPE.
349
+ const term = `%${escapeLike(value)}%`;
350
+ const rows = this.db
351
+ .query(`SELECT code, long_desc, short_desc FROM codes
352
+ WHERE system = 'RXNORM' AND (long_desc LIKE ? ESCAPE '\\' OR short_desc LIKE ? ESCAPE '\\')
353
+ ORDER BY length(code) LIMIT ?`)
354
+ .all(term, term, getServerConfig().maxResults);
355
+ if (rows.length === 0)
356
+ return { kind: 'source_not_found' };
357
+ return {
358
+ kind: 'ok',
359
+ resolvedSystem: 'RXNORM',
360
+ hits: rows.map((r) => ({
361
+ source: 'RXNORM',
362
+ system: 'RXNORM',
363
+ value: r.code,
364
+ ...(r.long_desc || r.short_desc
365
+ ? { description: (r.long_desc ?? r.short_desc) }
366
+ : {}),
367
+ })),
368
+ };
369
+ }
370
+ case 'ndc_to_rxcui': {
371
+ const ndc = value.replace(/[^0-9]/g, '');
372
+ const rows = this.db.query('SELECT rxcui FROM ndc_map WHERE ndc = ?').all(ndc);
373
+ if (rows.length === 0)
374
+ return { kind: 'source_not_found' };
375
+ return {
376
+ kind: 'ok',
377
+ resolvedSystem: 'RXNORM',
378
+ hits: rows.map((r) => ({ source: 'NDC', system: 'RXNORM', value: r.rxcui })),
379
+ };
380
+ }
381
+ case 'rxcui_to_ndc': {
382
+ const rows = this.db.query('SELECT ndc FROM ndc_map WHERE rxcui = ?').all(value);
383
+ if (rows.length === 0)
384
+ return { kind: 'source_not_found' };
385
+ return {
386
+ kind: 'ok',
387
+ resolvedSystem: 'RXNORM',
388
+ hits: rows.map((r) => ({ source: 'NDC', system: null, value: r.ndc })),
389
+ };
390
+ }
391
+ case 'rxcui_to_ingredients':
392
+ case 'rxcui_to_brands': {
393
+ const rel = direction === 'rxcui_to_ingredients' ? 'has_ingredient' : 'has_tradename';
394
+ const rows = this.db
395
+ .query('SELECT rel, target, target_type FROM rxnorm_rel WHERE rxcui = ? AND rel = ?')
396
+ .all(value, rel);
397
+ if (rows.length === 0)
398
+ return { kind: 'source_not_found' };
399
+ return {
400
+ kind: 'ok',
401
+ resolvedSystem: 'RXNORM',
402
+ hits: rows.map((r) => ({
403
+ source: r.rel,
404
+ system: 'RXNORM',
405
+ value: r.target,
406
+ ...(r.targetType ? { description: r.targetType } : {}),
407
+ })),
408
+ };
409
+ }
410
+ default:
411
+ return { kind: 'source_not_found' };
412
+ }
413
+ }
414
+ /** True when `direction` needs the RxNorm tables. */
415
+ static isDrugDirection(direction) {
416
+ return DRUG_DIRECTIONS.includes(direction);
417
+ }
418
+ /**
419
+ * Browse a system's hierarchy. With no `node`: top-level entries (3-char
420
+ * ICD-10-CM categories grouped by chapter letter, HCPCS range buckets, PCS
421
+ * section axis values). With a `node`: that node's children — for ICD-10-PCS,
422
+ * the valid next-position axis values rather than prefix children.
423
+ */
424
+ browse(system, node, limit) {
425
+ if (system === 'ICD10PCS') {
426
+ return this.browsePcs(node, limit);
427
+ }
428
+ if (!node) {
429
+ // Top level: codes with no parent (roots). For ICD10CM these are 3-char
430
+ // categories; for HCPCS the single-letter range buckets (if seeded) or the
431
+ // distinct chapters.
432
+ const rows = this.db
433
+ .query('SELECT * FROM codes WHERE system = ? AND parent IS NULL ORDER BY code LIMIT ?')
434
+ .all(system, limit);
435
+ return {
436
+ kind: 'codes',
437
+ codes: rows.map((r) => CodeIndexService.decode(CodeIndexService.toCodeRow(r))),
438
+ };
439
+ }
440
+ const code = storageCode(node);
441
+ if (!this.getRow(code, system))
442
+ return { kind: 'unknown_node' };
443
+ return { kind: 'codes', codes: this.childrenOf(system, code) };
444
+ }
445
+ /**
446
+ * PCS axis browse. Only the position-1 Section axis is baked into `pcs_axes`
447
+ * (positions 2-7 are context-dependent on the full preceding axis path and a
448
+ * flat table can't represent them without storing wrong meanings — see
449
+ * `parseIcd10pcsAxes`). So the top level (no node) returns the 17 Sections;
450
+ * a partial code asks for a deeper position, which has no rows — the caller
451
+ * gets an explicit notice (handled in the tool), not silent-empty or wrong data.
452
+ */
453
+ browsePcs(node, limit) {
454
+ const partial = node ? storageCode(node) : '';
455
+ if (partial.length >= 7)
456
+ return { kind: 'unknown_node' };
457
+ const position = partial.length + 1; // next axis position to enumerate
458
+ const rows = this.db
459
+ .query('SELECT position, value, meaning FROM pcs_axes WHERE position = ? ORDER BY value LIMIT ?')
460
+ .all(position, limit);
461
+ return { kind: 'axes', axes: rows };
462
+ }
463
+ /** Provenance for every bundled system. */
464
+ listSystems() {
465
+ const rows = this.db.query('SELECT * FROM build_meta ORDER BY system').all();
466
+ const order = new Map(SYSTEM_IDS.map((s, i) => [s, i]));
467
+ return rows
468
+ .map((r) => ({
469
+ system: r.system,
470
+ releaseId: r.release_id,
471
+ effectiveStart: r.effective_start ?? null,
472
+ effectiveEnd: r.effective_end ?? null,
473
+ codeCount: Number(r.code_count ?? 0),
474
+ sourceUrl: r.source_url ?? null,
475
+ builtAt: r.built_at,
476
+ }))
477
+ .sort((a, b) => (order.get(a.system) ?? 99) - (order.get(b.system) ?? 99));
478
+ }
479
+ /** Expose the decode projection for tools that already hold a CodeRow. */
480
+ static project(row) {
481
+ return CodeIndexService.decode(row);
482
+ }
483
+ }
484
+ /**
485
+ * Translate free user text into a safe FTS5 MATCH expression. Strips FTS5
486
+ * operator characters, splits into tokens, and ANDs prefix-matched tokens so
487
+ * "diabetic neuropathy" requires both stems. Returns null when nothing usable
488
+ * remains (caller treats that as an empty result, not an error).
489
+ */
490
+ export function toFtsMatch(text) {
491
+ const tokens = text
492
+ .toLowerCase()
493
+ .replace(/["()*:^-]/g, ' ')
494
+ .split(/\s+/)
495
+ .map((t) => t.trim())
496
+ .filter((t) => t.length > 0);
497
+ if (tokens.length === 0)
498
+ return null;
499
+ // Quote each token (defuses any residual special handling) and prefix-match.
500
+ return tokens.map((t) => `"${t}"*`).join(' AND ');
501
+ }
502
+ /**
503
+ * Escape SQL LIKE wildcards (`%`, `_`) and the escape char (`\`) in a
504
+ * user-supplied substring so it matches literally inside a `LIKE '%…%'` pattern.
505
+ * Pair with `ESCAPE '\'` in the query. Without this a value like `%` would match
506
+ * every row; `_` would match any single character.
507
+ */
508
+ export function escapeLike(value) {
509
+ return value.replace(/[\\%_]/g, (ch) => `\\${ch}`);
510
+ }
511
+ /** Format a YYYYMMDD storage date as YYYY-MM-DD; pass through anything else. */
512
+ function formatDate(yyyymmdd) {
513
+ return /^\d{8}$/.test(yyyymmdd)
514
+ ? `${yyyymmdd.slice(0, 4)}-${yyyymmdd.slice(4, 6)}-${yyyymmdd.slice(6, 8)}`
515
+ : yyyymmdd;
516
+ }
517
+ // ─── Init / Accessor ────────────────────────────────────────────────────────
518
+ let _service;
519
+ /** Open the bundled index and cache the handle. Called once from `setup()`. */
520
+ export async function initCodeIndexService() {
521
+ _service = await CodeIndexService.open();
522
+ }
523
+ /** Return the initialized service, throwing if `setup()` hasn't run. */
524
+ export function getCodeIndexService() {
525
+ if (!_service) {
526
+ throw new Error('CodeIndexService not initialized — call initCodeIndexService() in setup()');
527
+ }
528
+ return _service;
529
+ }
530
+ //# sourceMappingURL=code-index-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"code-index-service.js","sourceRoot":"","sources":["../../../src/services/code-index/code-index-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @fileoverview Code-shape auto-detection. Each bundled system has a distinct
3
+ * lexical shape; `detectSystems` returns every system a raw code could belong
4
+ * to so the caller can disambiguate (one match → route directly; multiple →
5
+ * `ambiguous_system`; zero → unknown shape).
6
+ * @module services/code-index/detect
7
+ */
8
+ import type { SystemId } from './types.js';
9
+ /**
10
+ * Return every system whose shape the raw code matches, in canonical order.
11
+ * Empty array ⇒ the code matches no known shape. The caller decides what a
12
+ * single vs. multiple match means.
13
+ *
14
+ * Note the deliberate overlaps the design calls out: a 7-character ICD-10-PCS
15
+ * code can also satisfy the ICD-10-CM pattern is NOT possible (CM caps at 3+4=7
16
+ * but requires digits in positions 2-3 and a leading A-Z excluding nothing),
17
+ * but a 5-char HCPCS code and a short ICD-10-CM category never collide because
18
+ * HCPCS requires 4 trailing digits while CM position 2-3 are digits and 4-5 are
19
+ * alphanumeric — `J0120` is HCPCS-only. The real ambiguity is a bare integer
20
+ * (RXCUI) vs nothing else, and a 7-char alphanumeric that is PCS-shaped.
21
+ */
22
+ export declare function detectSystems(rawCode: string): SystemId[];
23
+ //# sourceMappingURL=detect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detect.d.ts","sourceRoot":"","sources":["../../../src/services/code-index/detect.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AA2B3C;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,EAAE,CAUzD"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * @fileoverview Code-shape auto-detection. Each bundled system has a distinct
3
+ * lexical shape; `detectSystems` returns every system a raw code could belong
4
+ * to so the caller can disambiguate (one match → route directly; multiple →
5
+ * `ambiguous_system`; zero → unknown shape).
6
+ * @module services/code-index/detect
7
+ */
8
+ import { storageCode } from './schema.js';
9
+ /**
10
+ * ICD-10-CM: a letter, two digits, then optionally a dot and up to four more
11
+ * alphanumerics. Tested against the dot-stripped storage form. The first
12
+ * character is a letter; the next two are digits; trailing characters are
13
+ * alphanumeric. `D` and `U` are valid leading letters in ICD-10-CM.
14
+ */
15
+ const ICD10CM_RE = /^[A-TV-Z][0-9][0-9AB][0-9A-Z]{0,4}$/;
16
+ /**
17
+ * ICD-10-PCS: exactly 7 characters, alphanumeric, drawn from digits 0-9 and
18
+ * letters A-H,J-N,P-Z (the letters `I` and `O` are excluded by design to avoid
19
+ * confusion with 1 and 0).
20
+ */
21
+ const ICD10PCS_RE = /^[0-9A-HJ-NP-Z]{7}$/;
22
+ /**
23
+ * HCPCS Level II: one letter A-V, then exactly four digits (e.g. `J0120`,
24
+ * `E0110`). The leading-letter range A-V is what separates it from a generic
25
+ * 5-char alphanumeric.
26
+ */
27
+ const HCPCS_RE = /^[A-V][0-9]{4}$/;
28
+ /** RXCUI: a pure integer (RxNorm concept identifier). */
29
+ const RXCUI_RE = /^[0-9]+$/;
30
+ /**
31
+ * Return every system whose shape the raw code matches, in canonical order.
32
+ * Empty array ⇒ the code matches no known shape. The caller decides what a
33
+ * single vs. multiple match means.
34
+ *
35
+ * Note the deliberate overlaps the design calls out: a 7-character ICD-10-PCS
36
+ * code can also satisfy the ICD-10-CM pattern is NOT possible (CM caps at 3+4=7
37
+ * but requires digits in positions 2-3 and a leading A-Z excluding nothing),
38
+ * but a 5-char HCPCS code and a short ICD-10-CM category never collide because
39
+ * HCPCS requires 4 trailing digits while CM position 2-3 are digits and 4-5 are
40
+ * alphanumeric — `J0120` is HCPCS-only. The real ambiguity is a bare integer
41
+ * (RXCUI) vs nothing else, and a 7-char alphanumeric that is PCS-shaped.
42
+ */
43
+ export function detectSystems(rawCode) {
44
+ const code = storageCode(rawCode);
45
+ if (code.length === 0)
46
+ return [];
47
+ const matches = [];
48
+ if (ICD10CM_RE.test(code))
49
+ matches.push('ICD10CM');
50
+ if (ICD10PCS_RE.test(code))
51
+ matches.push('ICD10PCS');
52
+ if (HCPCS_RE.test(code))
53
+ matches.push('HCPCS');
54
+ if (RXCUI_RE.test(code))
55
+ matches.push('RXNORM');
56
+ return matches;
57
+ }
58
+ //# sourceMappingURL=detect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detect.js","sourceRoot":"","sources":["../../../src/services/code-index/detect.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG1C;;;;;GAKG;AACH,MAAM,UAAU,GAAG,qCAAqC,CAAC;AAEzD;;;;GAIG;AACH,MAAM,WAAW,GAAG,qBAAqB,CAAC;AAE1C;;;;GAIG;AACH,MAAM,QAAQ,GAAG,iBAAiB,CAAC;AAEnC,yDAAyD;AACzD,MAAM,QAAQ,GAAG,UAAU,CAAC;AAE5B;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,aAAa,CAAC,OAAe;IAC3C,MAAM,IAAI,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;IAClC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAEjC,MAAM,OAAO,GAAe,EAAE,CAAC;IAC/B,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACnD,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACrD,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC/C,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAChD,OAAO,OAAO,CAAC;AACjB,CAAC"}