@precisa-saude/fhir 0.33.0 → 0.35.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 (70) hide show
  1. package/README.md +2 -0
  2. package/dist/biomarkers.cjs +4 -2
  3. package/dist/biomarkers.cjs.map +1 -1
  4. package/dist/biomarkers.d.cts +34 -1
  5. package/dist/biomarkers.d.ts +34 -1
  6. package/dist/biomarkers.js +3 -1
  7. package/dist/{chunk-X5WVIJGG.js → chunk-6QUGBBV2.js} +12 -3
  8. package/dist/chunk-6QUGBBV2.js.map +1 -0
  9. package/dist/{chunk-ZCIZUBMR.cjs → chunk-77LDD6OP.cjs} +20 -11
  10. package/dist/chunk-77LDD6OP.cjs.map +1 -0
  11. package/dist/{chunk-LXKFVJF4.cjs → chunk-HZSW5T7E.cjs} +447 -63
  12. package/dist/chunk-HZSW5T7E.cjs.map +1 -0
  13. package/dist/{chunk-4FKZG5GZ.js → chunk-KQ4CX67G.js} +35 -1
  14. package/dist/chunk-KQ4CX67G.js.map +1 -0
  15. package/dist/{chunk-5RC7C7HJ.cjs → chunk-LNL5QSHP.cjs} +3 -3
  16. package/dist/{chunk-5RC7C7HJ.cjs.map → chunk-LNL5QSHP.cjs.map} +1 -1
  17. package/dist/{chunk-WXCX5TJJ.js → chunk-O3FXUPW3.js} +445 -61
  18. package/dist/chunk-O3FXUPW3.js.map +1 -0
  19. package/dist/{chunk-KS2YZBAS.js → chunk-TLF3IVSN.js} +14 -6
  20. package/dist/chunk-TLF3IVSN.js.map +1 -0
  21. package/dist/{chunk-Q3H5C6UR.js → chunk-U2XW6DY4.js} +2 -2
  22. package/dist/{chunk-OAFAERDY.cjs → chunk-VMYB7KOE.cjs} +3 -3
  23. package/dist/{chunk-OAFAERDY.cjs.map → chunk-VMYB7KOE.cjs.map} +1 -1
  24. package/dist/{chunk-FGZXQWSR.cjs → chunk-XAIIZLZ7.cjs} +16 -8
  25. package/dist/chunk-XAIIZLZ7.cjs.map +1 -0
  26. package/dist/{chunk-E6MXDQXW.js → chunk-XT635TWP.js} +2 -2
  27. package/dist/{chunk-T75NZM56.cjs → chunk-YL65SZ6S.cjs} +38 -4
  28. package/dist/chunk-YL65SZ6S.cjs.map +1 -0
  29. package/dist/cli.js +1276 -851
  30. package/dist/{converter-DZH9x52r.d.cts → converter-CTj83kPz.d.ts} +15 -1
  31. package/dist/{converter-D1Q84Oc9.d.ts → converter-Dlwb6OfB.d.cts} +15 -1
  32. package/dist/converter.cjs +5 -4
  33. package/dist/converter.cjs.map +1 -1
  34. package/dist/converter.d.cts +3 -2
  35. package/dist/converter.d.ts +3 -2
  36. package/dist/converter.js +4 -3
  37. package/dist/{fhir-types-B1KQmlRb.d.ts → fhir-types-Cn5WFbOI.d.cts} +5 -0
  38. package/dist/{fhir-types-B1KQmlRb.d.cts → fhir-types-Cn5WFbOI.d.ts} +5 -0
  39. package/dist/importer.cjs +5 -5
  40. package/dist/importer.d.cts +5 -1
  41. package/dist/importer.d.ts +5 -1
  42. package/dist/importer.js +4 -4
  43. package/dist/index.cjs +16 -10
  44. package/dist/index.cjs.map +1 -1
  45. package/dist/index.d.cts +6 -6
  46. package/dist/index.d.ts +6 -6
  47. package/dist/index.js +13 -7
  48. package/dist/index.js.map +1 -1
  49. package/dist/reference-ranges.cjs +8 -4
  50. package/dist/reference-ranges.cjs.map +1 -1
  51. package/dist/reference-ranges.d.cts +83 -1
  52. package/dist/reference-ranges.d.ts +83 -1
  53. package/dist/reference-ranges.js +9 -5
  54. package/dist/units.cjs +3 -3
  55. package/dist/units.js +2 -2
  56. package/dist/validators.cjs +4 -4
  57. package/dist/validators.d.cts +1 -1
  58. package/dist/validators.d.ts +1 -1
  59. package/dist/validators.js +3 -3
  60. package/package.json +1 -1
  61. package/dist/chunk-4FKZG5GZ.js.map +0 -1
  62. package/dist/chunk-FGZXQWSR.cjs.map +0 -1
  63. package/dist/chunk-KS2YZBAS.js.map +0 -1
  64. package/dist/chunk-LXKFVJF4.cjs.map +0 -1
  65. package/dist/chunk-T75NZM56.cjs.map +0 -1
  66. package/dist/chunk-WXCX5TJJ.js.map +0 -1
  67. package/dist/chunk-X5WVIJGG.js.map +0 -1
  68. package/dist/chunk-ZCIZUBMR.cjs.map +0 -1
  69. /package/dist/{chunk-Q3H5C6UR.js.map → chunk-U2XW6DY4.js.map} +0 -0
  70. /package/dist/{chunk-E6MXDQXW.js.map → chunk-XT635TWP.js.map} +0 -0
package/dist/cli.js CHANGED
@@ -115,6 +115,32 @@ var BIOMARKER_DEFINITIONS = [
115
115
  category: "coracao",
116
116
  code: "LDL",
117
117
  loinc: "2089-1",
118
+ // Os três irmãos de 2089-1 no eixo Method, conferidos ativos em out/2026.
119
+ // Medido em três dezenas de laudos reais, a única pista escrita foi a
120
+ // nota de rodapé de um laboratório norte-americano que declara o cálculo de
121
+ // Martin-Hopkins. Nenhum laudo brasileiro do corpus imprimiu "calculado",
122
+ // "direto" ou "Método:" perto do LDL, e por isso as outras duas variantes
123
+ // ficam sem pista até aparecer laudo que as afirme.
124
+ methodVariants: [
125
+ {
126
+ cues: { en: ["calculated using the Martin-Hopkins"], pt: [] },
127
+ loinc: "96259-7",
128
+ method: "Calculated.Martin-Hopkins",
129
+ note: 'Rodap\xE9 "LDL-C is now calculated using the Martin-Hopkins calculation", que menciona Friedewald s\xF3 para comparar. Por isso a pista \xE9 a frase afirmativa, e n\xE3o o nome do m\xE9todo.'
130
+ },
131
+ {
132
+ cues: { en: [], pt: [] },
133
+ loinc: "13457-7",
134
+ method: "Calculated",
135
+ note: "Friedewald. Sem pista: nenhum laudo do corpus afirmou o c\xE1lculo por escrito."
136
+ },
137
+ {
138
+ cues: { en: [], pt: [] },
139
+ loinc: "18262-6",
140
+ method: "Direct assay",
141
+ note: "Dosagem direta. Sem pista: nenhum laudo do corpus afirmou o m\xE9todo por escrito."
142
+ }
143
+ ],
118
144
  names: {
119
145
  en: ["LDL Cholesterol", "LDL", "Low-Density Lipoprotein"],
120
146
  pt: ["Colesterol LDL", "LDL", "LDL-Colesterol"]
@@ -3313,6 +3339,10 @@ for (const def of BIOMARKER_DEFINITIONS) {
3313
3339
  validLoincSet.add(alias);
3314
3340
  }
3315
3341
  }
3342
+ for (const variant of def.methodVariants ?? []) {
3343
+ loincToCodeMap.set(variant.loinc, def.code);
3344
+ validLoincSet.add(variant.loinc);
3345
+ }
3316
3346
  if (def.codeAliases) {
3317
3347
  for (const alias of def.codeAliases) {
3318
3348
  codeAliasToCanonicalMap.set(alias, def.code);
@@ -3324,6 +3354,9 @@ for (const def of BIOMARKER_DEFINITIONS) {
3324
3354
  function loincToCode(loinc) {
3325
3355
  return loincToCodeMap.get(loinc);
3326
3356
  }
3357
+ function methodVariantOf(code, loinc) {
3358
+ return codeToDefinitionMap.get(code)?.methodVariants?.find((v) => v.loinc === loinc);
3359
+ }
3327
3360
  function codeToLoinc(code) {
3328
3361
  return codeToLoincMap.get(code);
3329
3362
  }
@@ -4412,628 +4445,89 @@ function resolveUcum(unit, biomarkerCode) {
4412
4445
  return isUcumCode(unit) ? unit : void 0;
4413
4446
  }
4414
4447
 
4415
- // src/converter.ts
4416
- function interpretationCode(flag) {
4417
- switch (flag) {
4418
- case "H":
4419
- return "H";
4420
- // High
4421
- case "L":
4422
- return "L";
4423
- // Low
4424
- default:
4425
- return "N";
4426
- }
4427
- }
4428
- function interpretationDisplay(flag) {
4429
- switch (flag) {
4430
- case "H":
4431
- return "High";
4432
- case "L":
4433
- return "Low";
4434
- default:
4435
- return "Normal";
4436
- }
4437
- }
4438
- var SEX_CODING = {
4439
- female: {
4440
- code: "female",
4441
- display: "Female",
4442
- system: "http://hl7.org/fhir/administrative-gender"
4448
+ // src/reference-ranges.ts
4449
+ var biomarkerRangeDefinitions = {
4450
+ Albumin_Creatinine_Ratio: {
4451
+ default: { max: 30, min: 0, optimalMax: 20, optimalMin: 0, unit: "mg/g" },
4452
+ kind: "decision-threshold",
4453
+ source: "tietz-7ed-2015"
4443
4454
  },
4444
- male: { code: "male", display: "Male", system: "http://hl7.org/fhir/administrative-gender" }
4445
- };
4446
- var buildReferenceRanges = (observation, quantity) => {
4447
- const toRanges = (low, high, sex) => {
4448
- if (low === void 0 && high === void 0) return [];
4449
- return [
4455
+ Adiponectin: {
4456
+ default: { max: 26, min: 4, optimalMax: 20, optimalMin: 8, unit: "mcg/mL" },
4457
+ kind: "reference-interval",
4458
+ source: "tietz-7ed-2015"
4459
+ },
4460
+ ADMA: {
4461
+ default: { max: 0.7, min: 0.3, optimalMax: 0.55, optimalMin: 0.3, unit: "umol/L" },
4462
+ kind: "reference-interval",
4463
+ source: "nemeth-adma-2017"
4464
+ },
4465
+ AFP: {
4466
+ default: { max: 10, min: 0, optimalMax: 8, optimalMin: 0, unit: "ng/mL" },
4467
+ kind: "reference-interval",
4468
+ source: "sturgeon-nacb-2008"
4469
+ },
4470
+ Albumin_Globulin_Ratio: {
4471
+ default: { max: 2.5, min: 1, optimalMax: 2.2, optimalMin: 1.2, unit: "" },
4472
+ kind: "reference-interval",
4473
+ source: "tietz-7ed-2015"
4474
+ },
4475
+ Albumin: {
4476
+ default: { max: 5, min: 3.5, optimalMax: 4.8, optimalMin: 4, unit: "g/dL" },
4477
+ kind: "reference-interval",
4478
+ source: "pns-bioquimica-2019"
4479
+ },
4480
+ AlkalinePhosphatase: {
4481
+ default: { max: 147, min: 44, optimalMax: 120, optimalMin: 50, unit: "U/L" },
4482
+ kind: "reference-interval",
4483
+ source: "tietz-7ed-2015",
4484
+ variants: [
4450
4485
  {
4451
- ...sex === void 0 ? {} : { appliesTo: [{ coding: [SEX_CODING[sex]] }] },
4452
- ...high === void 0 ? {} : { high: quantity(high) },
4453
- ...low === void 0 ? {} : { low: quantity(low) }
4454
- }
4455
- ];
4456
- };
4457
- if (observation.referenceRanges && observation.referenceRanges.length > 0) {
4458
- return observation.referenceRanges.flatMap((r) => toRanges(r.low, r.high, r.appliesTo));
4459
- }
4460
- return toRanges(observation.referenceMin, observation.referenceMax);
4461
- };
4462
- function labObservationToFHIR(observation, patientId, laboratoryName) {
4463
- const loincCode = codeToLoinc(observation.biomarkerCode);
4464
- const sourceUnit = observation.unit || getDefaultUnit(observation.biomarkerCode) || observation.unit;
4465
- const ucumUnit = resolveUcum(sourceUnit, observation.biomarkerCode);
4466
- const quantity = (value) => ({
4467
- ...ucumUnit ? { code: ucumUnit, system: "http://unitsofmeasure.org" } : {},
4468
- ...sourceUnit ? { unit: sourceUnit } : {},
4469
- value
4470
- });
4471
- const isQualitative = observation.isQualitative || typeof observation.value === "string";
4472
- const fhirObs = {
4473
- category: [
4486
+ ageMin: 18,
4487
+ range: { max: 147, min: 44, optimalMax: 120, optimalMin: 50, unit: "U/L" },
4488
+ sex: "M"
4489
+ },
4474
4490
  {
4475
- coding: [
4476
- {
4477
- code: "laboratory",
4478
- display: "Laboratory",
4479
- system: "http://terminology.hl7.org/CodeSystem/observation-category"
4480
- }
4481
- ]
4491
+ ageMin: 18,
4492
+ range: { max: 104, min: 35, optimalMax: 90, optimalMin: 40, unit: "U/L" },
4493
+ sex: "F"
4482
4494
  }
4483
- ],
4484
- code: {
4485
- // Sem LOINC, o coding LOINC simplesmente não sai. Antes ia `99999-9`,
4486
- // que não é código LOINC nenhum: publicava sob `http://loinc.org` uma
4487
- // afirmação falsa, e quem consumisse o bundle confiando no system
4488
- // trataria aquilo como código de verdade. Composição corporal, densidade
4489
- // óssea e escore de cálcio não têm LOINC, e o certo é a lacuna explícita.
4490
- coding: [
4491
- ...loincCode ? [
4492
- {
4493
- code: loincCode,
4494
- display: observation.biomarkerName,
4495
- system: LOINC_SYSTEM
4496
- }
4497
- ] : [],
4498
- {
4499
- code: observation.biomarkerCode,
4500
- display: observation.biomarkerName,
4501
- system: BIOMARKER_CODE_SYSTEM
4502
- }
4503
- ],
4504
- text: observation.biomarkerName
4505
- },
4506
- effectiveDateTime: observation.collectionDate,
4507
- id: `${observation.reportId}-${observation.biomarkerCode}`,
4508
- interpretation: [
4495
+ ]
4496
+ },
4497
+ ALT: {
4498
+ default: { max: 35, min: 0, optimalMax: 25, optimalMin: 0, unit: "U/L" },
4499
+ kind: "reference-interval",
4500
+ source: "tietz-7ed-2015",
4501
+ variants: [
4509
4502
  {
4510
- coding: [
4511
- {
4512
- code: interpretationCode(observation.flag),
4513
- display: interpretationDisplay(observation.flag),
4514
- system: "http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation"
4515
- }
4516
- ]
4517
- }
4518
- ],
4519
- performer: laboratoryName ? [{ display: laboratoryName }] : void 0,
4520
- resourceType: "Observation",
4521
- status: "final",
4522
- subject: {
4523
- reference: `Patient/${patientId}`
4524
- }
4525
- };
4526
- if (isQualitative) {
4527
- fhirObs.valueString = String(observation.value);
4528
- } else {
4529
- fhirObs.valueQuantity = quantity(observation.value);
4530
- const referenceRange = buildReferenceRanges(observation, quantity);
4531
- if (referenceRange.length > 0) fhirObs.referenceRange = referenceRange;
4532
- }
4533
- return fhirObs;
4534
- }
4535
- function labReportToFHIR(report, patientId, observationIds) {
4536
- let status;
4537
- switch (report.processingStatus) {
4538
- case "complete":
4539
- status = "final";
4540
- break;
4541
- case "partial":
4542
- status = "partial";
4543
- break;
4544
- case "pending_review":
4545
- status = "preliminary";
4546
- break;
4547
- default:
4548
- status = "final";
4549
- }
4550
- return {
4551
- category: [
4503
+ ageMin: 18,
4504
+ range: { max: 56, min: 7, optimalMax: 35, optimalMin: 10, unit: "U/L" },
4505
+ sex: "M"
4506
+ },
4552
4507
  {
4553
- coding: [
4554
- {
4555
- code: "LAB",
4556
- display: "Laboratory",
4557
- system: "http://terminology.hl7.org/CodeSystem/v2-0074"
4558
- }
4559
- ]
4508
+ ageMin: 18,
4509
+ range: { max: 45, min: 7, optimalMax: 30, optimalMin: 10, unit: "U/L" },
4510
+ sex: "F"
4560
4511
  }
4561
- ],
4562
- code: {
4563
- coding: [
4564
- {
4565
- code: "11502-2",
4566
- // Laboratory report
4567
- display: "Laboratory report",
4568
- system: "http://loinc.org"
4569
- }
4570
- ],
4571
- text: "Laboratory Results"
4572
- },
4573
- conclusion: report.overallStatus === "NORMAL" ? "All results within normal limits" : "One or more abnormal results detected",
4574
- conclusionCode: report.overallStatus === "ANORMAL" ? [
4512
+ ]
4513
+ },
4514
+ // AMH — fortemente dependente de idade e sexo. As variantes femininas por
4515
+ // faixa etária são a referência clínica; o `default` foi ajustado para
4516
+ // refletir uma faixa adulta conservadora (~18-40 anos, mulheres em idade
4517
+ // reprodutiva geral). optimalMax anterior (6.9 ng/mL) sobrepunha-se a
4518
+ // valores sugestivos de SOP ou risco de OHSS; clinicamente, valores acima
4519
+ // de ~4 ng/mL já merecem investigação.
4520
+ AMH: {
4521
+ default: { max: 6, min: 1, optimalMax: 4, optimalMin: 1.5, unit: "ng/mL" },
4522
+ kind: "reference-interval",
4523
+ source: "tietz-7ed-2015",
4524
+ variants: [
4575
4525
  {
4576
- coding: [
4577
- {
4578
- code: "A",
4579
- display: "Abnormal",
4580
- system: "http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation"
4581
- }
4582
- ]
4583
- }
4584
- ] : void 0,
4585
- effectiveDateTime: report.collectionDate,
4586
- id: report.reportId,
4587
- issued: report.createdAt,
4588
- performer: report.laboratoryName ? [{ display: report.laboratoryName }] : void 0,
4589
- resourceType: "DiagnosticReport",
4590
- result: observationIds.map((id) => ({ reference: `Observation/${id}` })),
4591
- status,
4592
- subject: {
4593
- reference: `Patient/${patientId}`
4594
- }
4595
- };
4596
- }
4597
- function userProfileToFHIR(profile) {
4598
- const nameParts = profile.name.split(" ");
4599
- const given = nameParts.slice(0, -1);
4600
- const family = nameParts[nameParts.length - 1] || "";
4601
- return {
4602
- address: profile.address ? [
4603
- {
4604
- city: profile.address.city,
4605
- country: profile.address.country || "BR",
4606
- line: [
4607
- profile.address.street && profile.address.number ? `${profile.address.street}, ${profile.address.number}` : profile.address.street,
4608
- profile.address.complement
4609
- ].filter(Boolean),
4610
- postalCode: profile.address.postalCode,
4611
- state: profile.address.state
4612
- }
4613
- ] : void 0,
4614
- birthDate: profile.birthDate,
4615
- gender: profile.gender,
4616
- id: profile.userId,
4617
- name: [
4618
- {
4619
- family,
4620
- given: given.length > 0 ? given : void 0,
4621
- text: profile.name
4622
- }
4623
- ],
4624
- resourceType: "Patient",
4625
- telecom: [
4626
- ...profile.email ? [{ system: "email", value: profile.email }] : [],
4627
- ...profile.phone ? [{ system: "phone", value: profile.phone }] : []
4628
- ].length > 0 ? [
4629
- ...profile.email ? [{ system: "email", value: profile.email }] : [],
4630
- ...profile.phone ? [{ system: "phone", value: profile.phone }] : []
4631
- ] : void 0
4632
- };
4633
- }
4634
- function labResultToFHIRBundle(report, observations, userProfile) {
4635
- const patientId = userProfile.userId;
4636
- const fhirObservations = observations.map((obs) => {
4637
- const resource = labObservationToFHIR(
4638
- { ...obs, collectionDate: report.collectionDate },
4639
- patientId,
4640
- report.laboratoryName
4641
- );
4642
- return { fullUrl: entryFullUrl(resource), resource };
4643
- });
4644
- const observationIds = fhirObservations.map((entry) => entry.resource.id);
4645
- const diagnosticReport = labReportToFHIR(report, patientId, observationIds);
4646
- const fhirPatient = userProfileToFHIR(userProfile);
4647
- return {
4648
- entry: [
4649
- { fullUrl: entryFullUrl(fhirPatient), resource: fhirPatient },
4650
- { fullUrl: entryFullUrl(diagnosticReport), resource: diagnosticReport },
4651
- ...fhirObservations
4652
- ],
4653
- resourceType: "Bundle",
4654
- type: "collection"
4655
- };
4656
- }
4657
-
4658
- // src/cli/commands/convert.ts
4659
- async function convert(args, json) {
4660
- const raw = await getInput(args[0]);
4661
- const data = parseJson(
4662
- raw,
4663
- "JSON inv\xE1lido. Forne\xE7a um objeto com { report, observations, profile }."
4664
- );
4665
- if (!data.report || !data.observations || !data.profile) {
4666
- exitWithError(
4667
- 'Formato esperado: { "report": LabReportData, "observations": LabObservationData[], "profile": UserProfileData }'
4668
- );
4669
- }
4670
- const bundle = labResultToFHIRBundle(data.report, data.observations, data.profile);
4671
- if (json) {
4672
- outputJson(bundle);
4673
- } else {
4674
- outputText(JSON.stringify(bundle, null, 2));
4675
- }
4676
- }
4677
-
4678
- // src/validators.ts
4679
- function validateFHIRImportBundle(data) {
4680
- const errors = [];
4681
- if (!data || typeof data !== "object") {
4682
- errors.push({ details: "Input must be a JSON object", field: "root" });
4683
- return errors;
4684
- }
4685
- const bundle = data;
4686
- if (bundle.resourceType !== "Bundle") {
4687
- errors.push({
4688
- details: `Expected resourceType "Bundle", got "${String(bundle.resourceType)}"`,
4689
- field: "resourceType"
4690
- });
4691
- }
4692
- if (!Array.isArray(bundle.entry)) {
4693
- errors.push({ details: 'Bundle must contain an "entry" array', field: "entry" });
4694
- } else if (bundle.entry.length === 0) {
4695
- errors.push({ details: "Bundle entry array is empty", field: "entry" });
4696
- }
4697
- return errors;
4698
- }
4699
-
4700
- // src/importer.ts
4701
- var MAX_OBSERVATIONS = 5e3;
4702
- var MAX_FILE_SIZE = 15 * 1024 * 1024;
4703
- function resolveBiomarkerCode(observation) {
4704
- const coding = observation.code?.coding ?? [];
4705
- const loincCode = coding.find((c) => c.system === LOINC_SYSTEM)?.code;
4706
- const declaredCode = coding.find((c) => c.system === BIOMARKER_CODE_SYSTEM)?.code;
4707
- const seenCodes = [
4708
- ...loincCode ? [`LOINC ${loincCode}`] : [],
4709
- ...declaredCode ? [`biomarker code ${declaredCode}`] : []
4710
- ];
4711
- let reason;
4712
- if (seenCodes.length > 0) {
4713
- reason = `Unknown code: ${seenCodes.join(", ")}`;
4714
- } else if (coding.length > 0) {
4715
- const systems = [...new Set(coding.map((c) => c.system ?? "(sem system)"))];
4716
- reason = `No code in a supported system (found: ${systems.join(", ")})`;
4717
- } else {
4718
- reason = "No code found in observation coding";
4719
- }
4720
- const fromLoinc = loincCode ? loincToCode(loincCode) : void 0;
4721
- if (fromLoinc) return { internalCode: fromLoinc, loincCode, reason };
4722
- if (declaredCode && isValidCode(declaredCode)) {
4723
- const canonical = normalizeCode(declaredCode);
4724
- return { internalCode: canonical, loincCode: codeToLoinc(canonical), reason };
4725
- }
4726
- return { loincCode, reason };
4727
- }
4728
- function extractFlag(observation) {
4729
- const code = observation.interpretation?.[0]?.coding?.[0]?.code;
4730
- if (code === "H" || code === "HH") return "H";
4731
- if (code === "L" || code === "LL") return "L";
4732
- return "";
4733
- }
4734
- function extractObservationsFromBundle(bundle) {
4735
- const observations = [];
4736
- const skipped = [];
4737
- for (let i = 0; i < bundle.entry.length; i++) {
4738
- const entry = bundle.entry[i];
4739
- if (!entry.resource) {
4740
- skipped.push({ index: i, reason: "Entry has no resource" });
4741
- continue;
4742
- }
4743
- if (entry.resource.resourceType !== "Observation") {
4744
- continue;
4745
- }
4746
- if (observations.length >= MAX_OBSERVATIONS) {
4747
- skipped.push({ index: i, reason: `Maximum of ${MAX_OBSERVATIONS} observations exceeded` });
4748
- continue;
4749
- }
4750
- observations.push(entry.resource);
4751
- }
4752
- return { observations, skipped };
4753
- }
4754
- function mapFHIRObservationToInternal(observation, index) {
4755
- const { internalCode, loincCode, reason } = resolveBiomarkerCode(observation);
4756
- if (!internalCode) {
4757
- return {
4758
- skipped: { index, loincCode, reason, resourceType: "Observation" }
4759
- };
4760
- }
4761
- const definition = getDefinitionByCode(internalCode);
4762
- let value;
4763
- let unit = "";
4764
- let isQualitative = false;
4765
- if (observation.valueQuantity?.value !== void 0) {
4766
- value = observation.valueQuantity.value;
4767
- unit = observation.valueQuantity.unit || observation.valueQuantity.code || "";
4768
- } else if (observation.valueString) {
4769
- value = observation.valueString;
4770
- isQualitative = true;
4771
- } else {
4772
- return {
4773
- skipped: {
4774
- index,
4775
- loincCode,
4776
- reason: "Observation has no value (valueQuantity or valueString)",
4777
- resourceType: "Observation"
4778
- }
4779
- };
4780
- }
4781
- const collectionDate = observation.effectiveDateTime || observation.effectivePeriod?.start || "";
4782
- if (!collectionDate) {
4783
- return {
4784
- skipped: {
4785
- index,
4786
- loincCode,
4787
- reason: "Observation has no effectiveDateTime or effectivePeriod.start",
4788
- resourceType: "Observation"
4789
- }
4790
- };
4791
- }
4792
- let referenceMin;
4793
- let referenceMax;
4794
- if (observation.referenceRange?.[0]) {
4795
- referenceMin = observation.referenceRange[0].low?.value;
4796
- referenceMax = observation.referenceRange[0].high?.value;
4797
- }
4798
- const imported = {
4799
- biomarkerCode: internalCode,
4800
- biomarkerName: definition?.names.pt[0] || definition?.names.en[0] || observation.code.text || internalCode,
4801
- collectionDate,
4802
- flag: extractFlag(observation),
4803
- isQualitative,
4804
- loincCode,
4805
- referenceMax,
4806
- referenceMin,
4807
- unit: unit || definition?.unit || "",
4808
- value
4809
- };
4810
- return { observation: imported };
4811
- }
4812
- function processImportBundle(data) {
4813
- const validationErrors = validateFHIRImportBundle(data);
4814
- if (validationErrors.length > 0) {
4815
- return {
4816
- errors: validationErrors,
4817
- imported: [],
4818
- skipped: [],
4819
- totalProcessed: 0
4820
- };
4821
- }
4822
- const bundle = data;
4823
- const { observations, skipped } = extractObservationsFromBundle(bundle);
4824
- const imported = [];
4825
- const allSkipped = [...skipped];
4826
- for (let i = 0; i < observations.length; i++) {
4827
- const result = mapFHIRObservationToInternal(observations[i], i);
4828
- if ("observation" in result) {
4829
- imported.push(result.observation);
4830
- } else {
4831
- allSkipped.push(result.skipped);
4832
- }
4833
- }
4834
- return {
4835
- errors: [],
4836
- imported,
4837
- skipped: allSkipped,
4838
- totalProcessed: observations.length
4839
- };
4840
- }
4841
-
4842
- // src/cli/commands/import.ts
4843
- async function importBundle(args, json) {
4844
- const raw = await getInput(args[0]);
4845
- const bundle = parseJson(raw, "JSON inv\xE1lido.");
4846
- const result = processImportBundle(bundle);
4847
- if (json) {
4848
- outputJson(result);
4849
- return;
4850
- }
4851
- outputText(`Processados: ${result.totalProcessed}`);
4852
- outputText(`Importados: ${result.imported.length}`);
4853
- outputText(`Ignorados: ${result.skipped.length}`);
4854
- outputText(`Erros: ${result.errors.length}`);
4855
- if (result.imported.length > 0) {
4856
- outputText("\nObserva\xE7\xF5es importadas:");
4857
- for (const obs of result.imported) {
4858
- outputText(` ${obs.biomarkerCode} (${obs.loincCode}): ${obs.value} ${obs.unit}`);
4859
- }
4860
- }
4861
- if (result.errors.length > 0) {
4862
- outputText("\nErros:");
4863
- for (const err of result.errors) {
4864
- outputText(` ${err.field}: ${err.details}`);
4865
- }
4866
- }
4867
- }
4868
-
4869
- // src/cli/commands/list.ts
4870
- import { parseArgs } from "util";
4871
- async function list(args, json) {
4872
- const { values } = parseArgs({
4873
- args,
4874
- options: {
4875
- category: { type: "string" },
4876
- visible: { default: false, type: "boolean" }
4877
- },
4878
- strict: false
4879
- });
4880
- let defs;
4881
- if (values.category) {
4882
- defs = getBiomarkersByCategory(values.category);
4883
- if (defs.length === 0) {
4884
- exitWithError(`Categoria n\xE3o encontrada ou vazia: ${values.category}`);
4885
- }
4886
- } else if (values.visible) {
4887
- defs = getVisibleDefinitions();
4888
- } else {
4889
- defs = getAllDefinitions();
4890
- }
4891
- if (json) {
4892
- outputJson(defs);
4893
- return;
4894
- }
4895
- const rows = defs.map((d) => [
4896
- d.code,
4897
- d.loinc ?? "\u2014",
4898
- d.names.pt[0] ?? "",
4899
- d.unit ?? "\u2014",
4900
- Array.isArray(d.category) ? d.category.join(", ") : d.category
4901
- ]);
4902
- outputText(formatTable(["C\xF3digo", "LOINC", "Nome (pt)", "Unidade", "Categoria"], rows));
4903
- outputText(`
4904
- Total: ${defs.length} biomarcadores`);
4905
- }
4906
-
4907
- // src/cli/commands/loinc-map.ts
4908
- async function loincMap(_args, json) {
4909
- const defs = getAllDefinitions().filter((d) => d.loinc);
4910
- if (json) {
4911
- const map = Object.fromEntries(defs.map((d) => [d.loinc, d.code]));
4912
- outputJson(map);
4913
- return;
4914
- }
4915
- const rows = defs.map((d) => [d.loinc, d.code, d.names.pt[0] ?? d.names.en[0] ?? ""]);
4916
- outputText(formatTable(["LOINC", "C\xF3digo", "Nome (pt)"], rows));
4917
- outputText(`
4918
- Total: ${rows.length} mapeamentos`);
4919
- }
4920
-
4921
- // src/cli/commands/lookup.ts
4922
- function displayDefinition(def) {
4923
- const cat = Array.isArray(def.category) ? def.category.join(", ") : def.category;
4924
- outputText(
4925
- [
4926
- `Biomarcador: ${def.code}`,
4927
- ` LOINC: ${def.loinc ?? "\u2014"}`,
4928
- ` Nomes (pt): ${def.names.pt.join(", ")}`,
4929
- ` Nomes (en): ${def.names.en.join(", ")}`,
4930
- ` Categoria: ${cat}`,
4931
- ` Unidade: ${def.unit ?? "\u2014"}`,
4932
- ` Sexo: ${def.sex ?? "ambos"}`,
4933
- ` Vis\xEDvel: ${def.hidden ? "n\xE3o" : "sim"}`
4934
- ].join("\n")
4935
- );
4936
- }
4937
- async function lookup(args, json) {
4938
- const code = args[0];
4939
- if (!code) exitWithError("Uso: fhir-bio lookup <c\xF3digo>");
4940
- const def = getDefinitionByCode(code);
4941
- if (!def) exitWithError(`Biomarcador n\xE3o encontrado: ${code}`);
4942
- if (json) {
4943
- outputJson(def);
4944
- } else {
4945
- displayDefinition(def);
4946
- }
4947
- }
4948
- async function lookupLoinc(args, json) {
4949
- const loinc = args[0];
4950
- if (!loinc) exitWithError("Uso: fhir-bio lookup-loinc <loinc>");
4951
- const def = getDefinitionByLoinc(loinc);
4952
- if (!def) exitWithError(`Biomarcador n\xE3o encontrado para LOINC: ${loinc}`);
4953
- if (json) {
4954
- outputJson(def);
4955
- } else {
4956
- displayDefinition(def);
4957
- }
4958
- }
4959
-
4960
- // src/cli/commands/range.ts
4961
- import { parseArgs as parseArgs2 } from "util";
4962
-
4963
- // src/reference-ranges.ts
4964
- var biomarkerRangeDefinitions = {
4965
- Albumin_Creatinine_Ratio: {
4966
- default: { max: 30, min: 0, optimalMax: 20, optimalMin: 0, unit: "mg/g" },
4967
- source: "tietz-7ed-2015"
4968
- },
4969
- Adiponectin: {
4970
- default: { max: 26, min: 4, optimalMax: 20, optimalMin: 8, unit: "mcg/mL" },
4971
- source: "tietz-7ed-2015"
4972
- },
4973
- ADMA: {
4974
- default: { max: 0.7, min: 0.3, optimalMax: 0.55, optimalMin: 0.3, unit: "umol/L" },
4975
- source: "nemeth-adma-2017"
4976
- },
4977
- AFP: {
4978
- default: { max: 10, min: 0, optimalMax: 8, optimalMin: 0, unit: "ng/mL" },
4979
- source: "sturgeon-nacb-2008"
4980
- },
4981
- Albumin_Globulin_Ratio: {
4982
- default: { max: 2.5, min: 1, optimalMax: 2.2, optimalMin: 1.2, unit: "" },
4983
- source: "tietz-7ed-2015"
4984
- },
4985
- Albumin: {
4986
- default: { max: 5, min: 3.5, optimalMax: 4.8, optimalMin: 4, unit: "g/dL" },
4987
- source: "pns-bioquimica-2019"
4988
- },
4989
- AlkalinePhosphatase: {
4990
- default: { max: 147, min: 44, optimalMax: 120, optimalMin: 50, unit: "U/L" },
4991
- source: "tietz-7ed-2015",
4992
- variants: [
4993
- {
4994
- ageMin: 18,
4995
- range: { max: 147, min: 44, optimalMax: 120, optimalMin: 50, unit: "U/L" },
4996
- sex: "M"
4997
- },
4998
- {
4999
- ageMin: 18,
5000
- range: { max: 104, min: 35, optimalMax: 90, optimalMin: 40, unit: "U/L" },
5001
- sex: "F"
5002
- }
5003
- ]
5004
- },
5005
- ALT: {
5006
- default: { max: 35, min: 0, optimalMax: 25, optimalMin: 0, unit: "U/L" },
5007
- source: "tietz-7ed-2015",
5008
- variants: [
5009
- {
5010
- ageMin: 18,
5011
- range: { max: 56, min: 7, optimalMax: 35, optimalMin: 10, unit: "U/L" },
5012
- sex: "M"
5013
- },
5014
- {
5015
- ageMin: 18,
5016
- range: { max: 45, min: 7, optimalMax: 30, optimalMin: 10, unit: "U/L" },
5017
- sex: "F"
5018
- }
5019
- ]
5020
- },
5021
- // AMH — fortemente dependente de idade e sexo. As variantes femininas por
5022
- // faixa etária são a referência clínica; o `default` foi ajustado para
5023
- // refletir uma faixa adulta conservadora (~18-40 anos, mulheres em idade
5024
- // reprodutiva geral). optimalMax anterior (6.9 ng/mL) sobrepunha-se a
5025
- // valores sugestivos de SOP ou risco de OHSS; clinicamente, valores acima
5026
- // de ~4 ng/mL já merecem investigação.
5027
- AMH: {
5028
- default: { max: 6, min: 1, optimalMax: 4, optimalMin: 1.5, unit: "ng/mL" },
5029
- source: "tietz-7ed-2015",
5030
- variants: [
5031
- {
5032
- ageMax: 24,
5033
- ageMin: 18,
5034
- range: { max: 13, min: 1.5, optimalMax: 10, optimalMin: 3, unit: "ng/mL" },
5035
- sex: "F"
5036
- },
4526
+ ageMax: 24,
4527
+ ageMin: 18,
4528
+ range: { max: 13, min: 1.5, optimalMax: 10, optimalMin: 3, unit: "ng/mL" },
4529
+ sex: "F"
4530
+ },
5037
4531
  {
5038
4532
  ageMax: 30,
5039
4533
  ageMin: 25,
@@ -5061,14 +4555,17 @@ var biomarkerRangeDefinitions = {
5061
4555
  },
5062
4556
  Ammonia: {
5063
4557
  default: { max: 45, min: 15, optimalMax: 40, optimalMin: 20, unit: "umol/L" },
4558
+ kind: "reference-interval",
5064
4559
  source: "tietz-7ed-2015"
5065
4560
  },
5066
4561
  AntiThyroglobulin: {
5067
4562
  default: { max: 115, min: 0, optimalMax: 40, optimalMin: 0, unit: "IU/mL" },
4563
+ kind: "reference-interval",
5068
4564
  source: "tietz-7ed-2015"
5069
4565
  },
5070
4566
  AntiTPO: {
5071
4567
  default: { max: 34, min: 0, optimalMax: 9, optimalMin: 0, unit: "IU/mL" },
4568
+ kind: "reference-interval",
5072
4569
  source: "tietz-7ed-2015"
5073
4570
  },
5074
4571
  // Aortic valve calcium — no standardized clinical threshold; use mild cutoff
@@ -5076,23 +4573,28 @@ var biomarkerRangeDefinitions = {
5076
4573
  AorticValveCalcium: {
5077
4574
  default: { max: 99, min: 0, optimalMax: 0, optimalMin: 0, unit: "AU" },
5078
4575
  direction: "lower-better",
4576
+ kind: "decision-threshold",
5079
4577
  source: "rumberger-cac-1999"
5080
4578
  },
5081
4579
  ApoA1: {
5082
4580
  default: { max: 200, min: 100, optimalMax: 180, optimalMin: 120, unit: "mg/dL" },
4581
+ kind: "reference-interval",
5083
4582
  source: "contois-apoa1-1996"
5084
4583
  },
5085
4584
  ApoB: {
5086
4585
  default: { max: 90, min: 0, optimalMax: 70, optimalMin: 0, unit: "mg/dL" },
5087
4586
  direction: "lower-better",
4587
+ kind: "decision-threshold",
5088
4588
  source: "sbc-lipids-2025"
5089
4589
  },
5090
4590
  ApoCIII: {
5091
4591
  default: { max: 10, min: 0, optimalMax: 7, optimalMin: 0, unit: "mg/dL" },
4592
+ kind: "decision-threshold",
5092
4593
  source: "khetarpal-apociii-2016"
5093
4594
  },
5094
4595
  Omega6_AA: {
5095
4596
  default: { max: 15, min: 5, optimalMax: 12, optimalMin: 7, unit: "%" },
4597
+ kind: "decision-threshold",
5096
4598
  source: "simopoulos-omega-ratio-2002"
5097
4599
  },
5098
4600
  // Razão Ácido Araquidônico/EPA (sangue total) — `lower-better`: razão menor =
@@ -5105,16 +4607,19 @@ var biomarkerRangeDefinitions = {
5105
4607
  // dentro do intervalo. Sem `optimal*`: não há corte ótimo de AA/EPA com fonte.
5106
4608
  // Em telas de relatório único, prefira o intervalo impresso pelo laboratório.
5107
4609
  AA_EPA_Ratio: {
5108
- default: { max: 40.7, min: 3.7, unit: "" },
4610
+ default: { max: 40.7, min: 3.7, minKind: "display", unit: "" },
5109
4611
  direction: "lower-better",
4612
+ kind: "population",
5110
4613
  source: "torrissen-omega3-dbs-2025"
5111
4614
  },
5112
4615
  Arsenic: {
5113
4616
  default: { max: 35, min: 0, optimalMax: 20, optimalMin: 0, unit: "mcg/L" },
4617
+ kind: "decision-threshold",
5114
4618
  source: "nr7-pcmso-2020"
5115
4619
  },
5116
4620
  AST: {
5117
4621
  default: { max: 35, min: 0, optimalMax: 25, optimalMin: 0, unit: "U/L" },
4622
+ kind: "reference-interval",
5118
4623
  source: "tietz-7ed-2015",
5119
4624
  variants: [
5120
4625
  {
@@ -5133,6 +4638,7 @@ var biomarkerRangeDefinitions = {
5133
4638
  // Tabela 2 da PNS 2019. Ver o comentário em `Neutrophils_Abs`.
5134
4639
  Basophils_Abs: {
5135
4640
  default: { max: 0.072, min: 0, unit: "K/uL" },
4641
+ kind: "reference-interval",
5136
4642
  source: "pns-hemograma-2019",
5137
4643
  variants: [
5138
4644
  { ageMin: 18, range: { max: 0.062, min: 0, unit: "K/uL" }, sex: "M" },
@@ -5141,6 +4647,7 @@ var biomarkerRangeDefinitions = {
5141
4647
  },
5142
4648
  Bicarbonate: {
5143
4649
  default: { max: 29, min: 23, optimalMax: 28, optimalMin: 24, unit: "mEq/L" },
4650
+ kind: "reference-interval",
5144
4651
  source: "tietz-7ed-2015"
5145
4652
  },
5146
4653
  // β-Hidroxibutirato (BHB) sérico — corpos cetônicos. Faixa derivada de
@@ -5150,23 +4657,28 @@ var biomarkerRangeDefinitions = {
5150
4657
  // superior aqui reflete estado pós-prandial habitual, não jejum.
5151
4658
  BetaHydroxybutyrate: {
5152
4659
  default: { max: 0.28, min: 0.02, optimalMax: 0.28, optimalMin: 0.02, unit: "mmol/L" },
4660
+ kind: "reference-interval",
5153
4661
  source: "klee-bhb-2020"
5154
4662
  },
5155
4663
  BilirubinDirect: {
5156
4664
  default: { max: 0.3, min: 0, optimalMax: 0.2, optimalMin: 0, unit: "mg/dL" },
4665
+ kind: "reference-interval",
5157
4666
  source: "tietz-7ed-2015"
5158
4667
  },
5159
4668
  BilirubinIndirect: {
5160
4669
  default: { max: 0.8, min: 0, optimalMax: 0.6, optimalMin: 0, unit: "mg/dL" },
4670
+ kind: "reference-interval",
5161
4671
  source: "tietz-7ed-2015"
5162
4672
  },
5163
4673
  BilirubinTotal: {
5164
4674
  default: { max: 1.2, min: 0.1, optimalMax: 0.9, optimalMin: 0.2, unit: "mg/dL" },
4675
+ kind: "reference-interval",
5165
4676
  source: "tietz-7ed-2015"
5166
4677
  },
5167
4678
  // BNP: 35 pg/mL = corte não-agudo para triagem de IC (SBC IC 2018)
5168
4679
  BNP: {
5169
4680
  default: { max: 35, min: 0, optimalMax: 20, optimalMin: 0, unit: "pg/mL" },
4681
+ kind: "decision-threshold",
5170
4682
  source: "sbc-ic-2018"
5171
4683
  },
5172
4684
  // Razão BUN/Creatinina — faixa 10-20 aplica-se ao Nitrogênio Ureico (BUN).
@@ -5177,14 +4689,17 @@ var biomarkerRangeDefinitions = {
5177
4689
  // distinta. Ver issue #41 para alinhamento de nomenclatura/LOINC.
5178
4690
  BUN_Creatinine_Ratio: {
5179
4691
  default: { max: 20, min: 10, optimalMax: 18, optimalMin: 12, unit: "" },
4692
+ kind: "reference-interval",
5180
4693
  source: "tietz-7ed-2015"
5181
4694
  },
5182
4695
  CA125: {
5183
4696
  default: { max: 35, min: 0, optimalMax: 25, optimalMin: 0, unit: "U/mL" },
4697
+ kind: "reference-interval",
5184
4698
  source: "sturgeon-nacb-2008"
5185
4699
  },
5186
4700
  CA199: {
5187
4701
  default: { max: 37, min: 0, optimalMax: 30, optimalMin: 0, unit: "U/mL" },
4702
+ kind: "reference-interval",
5188
4703
  source: "sturgeon-nacb-2008"
5189
4704
  },
5190
4705
  // CAC: Agatston classification — 0=none, 1-99=mild, 100-399=moderate, 400+=severe
@@ -5192,44 +4707,53 @@ var biomarkerRangeDefinitions = {
5192
4707
  CAC: {
5193
4708
  default: { max: 99, min: 0, optimalMax: 0, optimalMin: 0, unit: "AU" },
5194
4709
  direction: "lower-better",
4710
+ kind: "decision-threshold",
5195
4711
  source: "rumberger-cac-1999"
5196
4712
  },
5197
4713
  // Per-vessel scores — same Agatston classification applied per vessel
5198
4714
  CAC_LAD: {
5199
4715
  default: { max: 99, min: 0, optimalMax: 0, optimalMin: 0, unit: "AU" },
5200
4716
  direction: "lower-better",
4717
+ kind: "decision-threshold",
5201
4718
  source: "rumberger-cac-1999"
5202
4719
  },
5203
4720
  CAC_LCX: {
5204
4721
  default: { max: 99, min: 0, optimalMax: 0, optimalMin: 0, unit: "AU" },
5205
4722
  direction: "lower-better",
4723
+ kind: "decision-threshold",
5206
4724
  source: "rumberger-cac-1999"
5207
4725
  },
5208
4726
  CAC_LMA: {
5209
4727
  default: { max: 99, min: 0, optimalMax: 0, optimalMin: 0, unit: "AU" },
5210
4728
  direction: "lower-better",
4729
+ kind: "decision-threshold",
5211
4730
  source: "rumberger-cac-1999"
5212
4731
  },
5213
4732
  CAC_Percentile: {
5214
4733
  default: { max: 50, min: 0, optimalMax: 25, optimalMin: 0, unit: "%", warningMax: 75 },
5215
4734
  direction: "lower-better",
4735
+ kind: "decision-threshold",
5216
4736
  source: "rumberger-cac-1999"
5217
4737
  },
5218
4738
  CAC_RCA: {
5219
4739
  default: { max: 99, min: 0, optimalMax: 0, optimalMin: 0, unit: "AU" },
5220
4740
  direction: "lower-better",
4741
+ kind: "decision-threshold",
5221
4742
  source: "rumberger-cac-1999"
5222
4743
  },
5223
4744
  Cadmium: {
5224
4745
  default: { max: 1.2, min: 0, optimalMax: 0.5, optimalMin: 0, unit: "mcg/L" },
4746
+ kind: "decision-threshold",
5225
4747
  source: "nr7-pcmso-2020"
5226
4748
  },
5227
4749
  Calcium: {
5228
4750
  default: { max: 10.5, min: 8.5, optimalMax: 10, optimalMin: 9, unit: "mg/dL" },
4751
+ kind: "reference-interval",
5229
4752
  source: "tietz-7ed-2015"
5230
4753
  },
5231
4754
  CEA: {
5232
4755
  default: { max: 3, min: 0, optimalMax: 2.5, optimalMin: 0, unit: "ng/mL" },
4756
+ kind: "reference-interval",
5233
4757
  source: "sturgeon-nacb-2008"
5234
4758
  },
5235
4759
  // CK: valores conservadores baseados em faixas laboratoriais típicas
@@ -5254,6 +4778,7 @@ var biomarkerRangeDefinitions = {
5254
4778
  // autodeclaração de raça/Cor". Ver a issue #3 para a decisão registrada.
5255
4779
  CK: {
5256
4780
  default: { max: 314, min: 30, optimalMax: 170, optimalMin: 40, unit: "U/L" },
4781
+ kind: "reference-interval",
5257
4782
  source: "kalaria-ck-ri-2026",
5258
4783
  variants: [
5259
4784
  { sex: "F", range: { max: 170, min: 30, optimalMax: 150, optimalMin: 40, unit: "U/L" } }
@@ -5261,6 +4786,7 @@ var biomarkerRangeDefinitions = {
5261
4786
  },
5262
4787
  Chloride: {
5263
4788
  default: { max: 106, min: 98, optimalMax: 105, optimalMin: 100, unit: "mEq/L" },
4789
+ kind: "reference-interval",
5264
4790
  source: "tietz-7ed-2015"
5265
4791
  },
5266
4792
  // Colesterol total — SBC 2017/2025: valor desejável < 190 mg/dL para adultos,
@@ -5268,6 +4794,7 @@ var biomarkerRangeDefinitions = {
5268
4794
  // (NCEP, 2002), superada pelas atualizações brasileiras.
5269
4795
  Cholesterol: {
5270
4796
  default: { max: 190, min: 0, optimalMax: 170, optimalMin: 0, unit: "mg/dL" },
4797
+ kind: "decision-threshold",
5271
4798
  source: "sbc-lipids-2025"
5272
4799
  },
5273
4800
  // Índice de Castelli I (CT/HDL-c)
@@ -5275,6 +4802,7 @@ var biomarkerRangeDefinitions = {
5275
4802
  // Índice de Castelli I — SBC 2017: M <4.9, F <4.3
5276
4803
  default: { max: 4.9, min: 0, optimalMax: 3.5, optimalMin: 0, unit: "" },
5277
4804
  direction: "lower-better",
4805
+ kind: "decision-threshold",
5278
4806
  source: "castelli-ratio-1992",
5279
4807
  variants: [
5280
4808
  {
@@ -5291,22 +4819,27 @@ var biomarkerRangeDefinitions = {
5291
4819
  },
5292
4820
  CO2: {
5293
4821
  default: { max: 29, min: 23, optimalMax: 28, optimalMin: 24, unit: "mEq/L" },
4822
+ kind: "reference-interval",
5294
4823
  source: "tietz-7ed-2015"
5295
4824
  },
5296
4825
  Copper: {
5297
4826
  default: { max: 175, min: 70, optimalMax: 150, optimalMin: 85, unit: "mcg/dL" },
4827
+ kind: "reference-interval",
5298
4828
  source: "tietz-7ed-2015"
5299
4829
  },
5300
4830
  CoQ10: {
5301
4831
  default: { max: 1.5, min: 0.5, optimalMax: 1.3, optimalMin: 0.7, unit: "mg/L" },
4832
+ kind: "reference-interval",
5302
4833
  source: "tietz-7ed-2015"
5303
4834
  },
5304
4835
  Cortisol: {
5305
4836
  default: { max: 25, min: 5, optimalMax: 20, optimalMin: 10, unit: "mcg/dL" },
4837
+ kind: "reference-interval",
5306
4838
  source: "tietz-7ed-2015"
5307
4839
  },
5308
4840
  CortisolFree: {
5309
4841
  default: { max: 2.5, min: 0.5, optimalMax: 2, optimalMin: 0.8, unit: "mcg/dL" },
4842
+ kind: "reference-interval",
5310
4843
  source: "tietz-7ed-2015"
5311
4844
  },
5312
4845
  CPeptide: {
@@ -5318,10 +4851,12 @@ var biomarkerRangeDefinitions = {
5318
4851
  optimalMin: 1,
5319
4852
  unit: "ng/mL"
5320
4853
  },
4854
+ kind: "reference-interval",
5321
4855
  source: "tietz-7ed-2015"
5322
4856
  },
5323
4857
  Creatinine: {
5324
4858
  default: { max: 1.2, min: 0.6, optimalMax: 1, optimalMin: 0.7, unit: "mg/dL" },
4859
+ kind: "reference-interval",
5325
4860
  source: "pns-bioquimica-2019",
5326
4861
  variants: [
5327
4862
  // Gestação: hiperfiltração glomerular (↑ 40–50% GFR) reduz a creatinina
@@ -5347,10 +4882,12 @@ var biomarkerRangeDefinitions = {
5347
4882
  CRP: {
5348
4883
  default: { max: 3, min: 0, optimalMax: 1, optimalMin: 0, unit: "mg/L" },
5349
4884
  direction: "lower-better",
4885
+ kind: "decision-threshold",
5350
4886
  source: "tietz-7ed-2015"
5351
4887
  },
5352
4888
  CystatinC: {
5353
4889
  default: { max: 1, min: 0.5, optimalMax: 0.9, optimalMin: 0.6, unit: "mg/L" },
4890
+ kind: "reference-interval",
5354
4891
  source: "tietz-7ed-2015"
5355
4892
  },
5356
4893
  // D-Dímero — exame de exclusão de TEV (tromboembolismo venoso) com
@@ -5364,6 +4901,7 @@ var biomarkerRangeDefinitions = {
5364
4901
  DDimer: {
5365
4902
  default: { max: 500, min: 0, unit: "ng/mL" },
5366
4903
  direction: "lower-better",
4904
+ kind: "decision-threshold",
5367
4905
  source: "wells-ddimer-2003",
5368
4906
  variants: [
5369
4907
  // Corte ajustado por idade — ESC 2019 (Konstantinides et al.) e
@@ -5377,10 +4915,12 @@ var biomarkerRangeDefinitions = {
5377
4915
  },
5378
4916
  Omega3_DHA: {
5379
4917
  default: { max: 8, min: 2, optimalMax: 6.5, optimalMin: 3.5, unit: "%" },
4918
+ kind: "decision-threshold",
5380
4919
  source: "harris-omega3-2004"
5381
4920
  },
5382
4921
  DHEAS: {
5383
4922
  default: { max: 500, min: 100, optimalMax: 400, optimalMin: 150, unit: "mcg/dL" },
4923
+ kind: "reference-interval",
5384
4924
  source: "tietz-7ed-2015",
5385
4925
  variants: [
5386
4926
  {
@@ -5421,20 +4961,30 @@ var biomarkerRangeDefinitions = {
5421
4961
  },
5422
4962
  Omega3_DPA: {
5423
4963
  default: { max: 2, min: 0.3, optimalMax: 1.5, optimalMin: 0.5, unit: "%" },
4964
+ kind: "decision-threshold",
5424
4965
  source: "harris-omega3-2004"
5425
4966
  },
5426
4967
  eGFR: {
5427
- default: { max: 120, min: 60, optimalMax: 120, optimalMin: 90, unit: "mL/min/1.73m\xB2" },
4968
+ default: {
4969
+ max: 120,
4970
+ maxKind: "display",
4971
+ min: 60,
4972
+ optimalMax: 120,
4973
+ optimalMin: 90,
4974
+ unit: "mL/min/1.73m\xB2"
4975
+ },
5428
4976
  // KDIGO 2024: TFG 60-89 (G2) sem marcador de lesão renal não é DRC, em
5429
4977
  // nenhuma faixa etária. A variante por idade que existia aqui rebaixava o
5430
4978
  // piso só para 60+ e ainda limitava o teto a 90, o que sinalizava como
5431
4979
  // alterado qualquer idoso com função preservada.
4980
+ kind: "decision-threshold",
5432
4981
  source: "kdigo-ckd-2024"
5433
4982
  },
5434
4983
  // `Eosinophils` (%) fica sem faixa de propósito e `Eosinophils_Abs` usa os limites da
5435
4984
  // Tabela 2 da PNS 2019. Ver o comentário em `Neutrophils_Abs`.
5436
4985
  Eosinophils_Abs: {
5437
4986
  default: { max: 0.66, min: 0, unit: "K/uL" },
4987
+ kind: "reference-interval",
5438
4988
  source: "pns-hemograma-2019",
5439
4989
  variants: [
5440
4990
  { ageMin: 18, range: { max: 0.66, min: 0, unit: "K/uL" }, sex: "M" },
@@ -5443,18 +4993,22 @@ var biomarkerRangeDefinitions = {
5443
4993
  },
5444
4994
  Omega3_EPA: {
5445
4995
  default: { max: 3.5, min: 0.5, optimalMax: 2.5, optimalMin: 1, unit: "%" },
4996
+ kind: "decision-threshold",
5446
4997
  source: "harris-omega3-2004"
5447
4998
  },
5448
4999
  EPADPADHA: {
5449
5000
  default: { max: 10, min: 3, optimalMax: 9, optimalMin: 5, unit: "%" },
5001
+ kind: "decision-threshold",
5450
5002
  source: "harris-omega3-2004"
5451
5003
  },
5452
5004
  ESR: {
5453
5005
  default: { max: 20, min: 0, optimalMax: 10, optimalMin: 0, unit: "mm/hr" },
5006
+ kind: "reference-interval",
5454
5007
  source: "tietz-7ed-2015"
5455
5008
  },
5456
5009
  Estradiol: {
5457
5010
  default: { max: 40, min: 10, optimalMax: 35, optimalMin: 15, unit: "pg/mL" },
5011
+ kind: "reference-interval",
5458
5012
  source: "tietz-7ed-2015",
5459
5013
  variants: [
5460
5014
  {
@@ -5477,6 +5031,7 @@ var biomarkerRangeDefinitions = {
5477
5031
  },
5478
5032
  F2Isoprostanes: {
5479
5033
  default: { max: 86, min: 0, optimalMax: 60, optimalMin: 0, unit: "pg/mL" },
5034
+ kind: "reference-interval",
5480
5035
  source: "tietz-7ed-2015"
5481
5036
  },
5482
5037
  // Ferritina — WHO 2020 é o documento de referência específico para avaliação
@@ -5486,6 +5041,7 @@ var biomarkerRangeDefinitions = {
5486
5041
  // tipo de faixa, mas documentado aqui para consumidores).
5487
5042
  Ferritin: {
5488
5043
  default: { max: 150, min: 12, optimalMax: 120, optimalMin: 30, unit: "ng/mL" },
5044
+ kind: "decision-threshold",
5489
5045
  source: "who-iron-2020",
5490
5046
  variants: [
5491
5047
  // Gestação: ferritina cai fisiologicamente no 2º/3º trimestre pela expansão
@@ -5518,14 +5074,17 @@ var biomarkerRangeDefinitions = {
5518
5074
  // Fibrinogênio: 200-400 mg/dL — faixa de referência padrão (método de Clauss)
5519
5075
  Fibrinogen: {
5520
5076
  default: { max: 400, min: 200, optimalMax: 350, optimalMin: 250, unit: "mg/dL" },
5077
+ kind: "reference-interval",
5521
5078
  source: "tietz-7ed-2015"
5522
5079
  },
5523
5080
  FolicAcid: {
5524
5081
  default: { max: 20, min: 3, optimalMax: 15, optimalMin: 5, unit: "ng/mL" },
5082
+ kind: "reference-interval",
5525
5083
  source: "tietz-7ed-2015"
5526
5084
  },
5527
5085
  FSH: {
5528
5086
  default: { max: 12.4, min: 1.5, optimalMax: 10, optimalMin: 3, unit: "mIU/mL" },
5087
+ kind: "reference-interval",
5529
5088
  source: "tietz-7ed-2015",
5530
5089
  variants: [
5531
5090
  {
@@ -5549,26 +5108,29 @@ var biomarkerRangeDefinitions = {
5549
5108
  GGT: {
5550
5109
  default: { max: 55, min: 0, optimalMax: 30, optimalMin: 0, unit: "U/L" },
5551
5110
  direction: "lower-better",
5111
+ kind: "reference-interval",
5552
5112
  source: "tietz-7ed-2015",
5553
5113
  variants: [
5554
5114
  {
5555
5115
  ageMin: 18,
5556
- range: { max: 61, min: 8, optimalMax: 40, optimalMin: 10, unit: "U/L" },
5116
+ range: { max: 61, min: 8, minKind: "display", optimalMax: 40, optimalMin: 10, unit: "U/L" },
5557
5117
  sex: "M"
5558
5118
  },
5559
5119
  {
5560
5120
  ageMin: 18,
5561
- range: { max: 36, min: 5, optimalMax: 25, optimalMin: 8, unit: "U/L" },
5121
+ range: { max: 36, min: 5, minKind: "display", optimalMax: 25, optimalMin: 8, unit: "U/L" },
5562
5122
  sex: "F"
5563
5123
  }
5564
5124
  ]
5565
5125
  },
5566
5126
  Ghrelin: {
5567
5127
  default: { max: 1e3, min: 300, optimalMax: 800, optimalMin: 400, unit: "pg/mL" },
5128
+ kind: "reference-interval",
5568
5129
  source: "tietz-7ed-2015"
5569
5130
  },
5570
5131
  Globulin: {
5571
5132
  default: { max: 3.5, min: 2, optimalMax: 3.2, optimalMin: 2.3, unit: "g/dL" },
5133
+ kind: "reference-interval",
5572
5134
  source: "tietz-7ed-2015"
5573
5135
  },
5574
5136
  // A faixa é de jejum, então mora na definição que afirma jejum. Glicose
@@ -5591,6 +5153,7 @@ var biomarkerRangeDefinitions = {
5591
5153
  optimalMin: 70,
5592
5154
  unit: "mg/dL"
5593
5155
  },
5156
+ kind: "decision-threshold",
5594
5157
  source: "sbd-diabetes-2024",
5595
5158
  variants: [
5596
5159
  // Gestação: DMG (IADPSG/SBD 2024) usa cortes mais restritivos na glicemia
@@ -5612,10 +5175,12 @@ var biomarkerRangeDefinitions = {
5612
5175
  },
5613
5176
  GlycoMark: {
5614
5177
  default: { max: 40, min: 10, optimalMax: 35, optimalMin: 15, unit: "mcg/mL" },
5178
+ kind: "reference-interval",
5615
5179
  source: "tietz-7ed-2015"
5616
5180
  },
5617
5181
  GrowthHormone: {
5618
5182
  default: { max: 5, min: 0, optimalMax: 3, optimalMin: 0, unit: "ng/mL" },
5183
+ kind: "reference-interval",
5619
5184
  source: "tietz-7ed-2015"
5620
5185
  },
5621
5186
  // HbA1c — SBD 2024 não estabelece piso de referência clinicamente acionável.
@@ -5625,11 +5190,13 @@ var biomarkerRangeDefinitions = {
5625
5190
  // criar flag clínico em valores fisiologicamente baixos. optimalMin preserva
5626
5191
  // o alvo fisiológico da fração glicada.
5627
5192
  HbA1c: {
5628
- default: { max: 5.7, min: 2, optimalMax: 5.3, optimalMin: 4.5, unit: "%" },
5193
+ default: { max: 5.7, min: 2, minKind: "display", optimalMax: 5.3, optimalMin: 4.5, unit: "%" },
5194
+ kind: "decision-threshold",
5629
5195
  source: "sbd-diabetes-2024"
5630
5196
  },
5631
5197
  Hct: {
5632
5198
  default: { max: 50, min: 36, optimalMax: 48, optimalMin: 40, unit: "%" },
5199
+ kind: "reference-interval",
5633
5200
  source: "pns-hemograma-2019",
5634
5201
  variants: [
5635
5202
  {
@@ -5651,18 +5218,40 @@ var biomarkerRangeDefinitions = {
5651
5218
  // não como ponto de corte clínico; consumidores devem tratar `direction:
5652
5219
  // higher-better` como sinal autoritativo.
5653
5220
  HDL: {
5654
- default: { max: 100, min: 40, optimalMax: 100, optimalMin: 50, unit: "mg/dL" },
5221
+ default: {
5222
+ max: 100,
5223
+ maxKind: "display",
5224
+ min: 40,
5225
+ optimalMax: 100,
5226
+ optimalMin: 50,
5227
+ unit: "mg/dL"
5228
+ },
5655
5229
  direction: "higher-better",
5230
+ kind: "decision-threshold",
5656
5231
  source: "sbc-lipids-2025",
5657
5232
  variants: [
5658
5233
  {
5659
5234
  ageMin: 18,
5660
- range: { max: 100, min: 40, optimalMax: 100, optimalMin: 45, unit: "mg/dL" },
5235
+ range: {
5236
+ max: 100,
5237
+ maxKind: "display",
5238
+ min: 40,
5239
+ optimalMax: 100,
5240
+ optimalMin: 45,
5241
+ unit: "mg/dL"
5242
+ },
5661
5243
  sex: "M"
5662
5244
  },
5663
5245
  {
5664
5246
  ageMin: 18,
5665
- range: { max: 100, min: 50, optimalMax: 100, optimalMin: 55, unit: "mg/dL" },
5247
+ range: {
5248
+ max: 100,
5249
+ maxKind: "display",
5250
+ min: 50,
5251
+ optimalMax: 100,
5252
+ optimalMin: 55,
5253
+ unit: "mg/dL"
5254
+ },
5666
5255
  sex: "F"
5667
5256
  }
5668
5257
  ]
@@ -5670,12 +5259,21 @@ var biomarkerRangeDefinitions = {
5670
5259
  HDL_Large: {
5671
5260
  // HDL Large: higher is better
5672
5261
  // Quest Ion Mobility reference: Male 4334-10815, Female 5038-17886 nmol/L, optimal >6729
5673
- default: { max: 17886, min: 4334, optimalMax: 17886, optimalMin: 6729, unit: "nmol/L" },
5262
+ default: {
5263
+ max: 17886,
5264
+ maxKind: "display",
5265
+ min: 4334,
5266
+ optimalMax: 17886,
5267
+ optimalMin: 6729,
5268
+ unit: "nmol/L"
5269
+ },
5674
5270
  direction: "higher-better",
5271
+ kind: "reference-interval",
5675
5272
  source: "caulfield-ionmobility-2008"
5676
5273
  },
5677
5274
  Hgb: {
5678
5275
  default: { max: 17.5, min: 12, optimalMax: 16, optimalMin: 13.5, unit: "g/dL" },
5276
+ kind: "reference-interval",
5679
5277
  source: "pns-hemograma-2019",
5680
5278
  variants: [
5681
5279
  // Gestação: hemodiluição fisiológica reduz o piso aceitável.
@@ -5731,19 +5329,23 @@ var biomarkerRangeDefinitions = {
5731
5329
  warningMax: 2.71
5732
5330
  },
5733
5331
  direction: "lower-better",
5332
+ kind: "decision-threshold",
5734
5333
  source: "geloneze-brams-2009"
5735
5334
  },
5736
5335
  Homocysteine: {
5737
- default: { max: 15, min: 4, optimalMax: 10, optimalMin: 5, unit: "umol/L" },
5336
+ default: { max: 15, min: 4, minKind: "display", optimalMax: 10, optimalMin: 5, unit: "umol/L" },
5738
5337
  direction: "lower-better",
5338
+ kind: "reference-interval",
5739
5339
  source: "selhub-homocysteine-1999"
5740
5340
  },
5741
5341
  IGF1: {
5742
5342
  default: { max: 350, min: 100, optimalMax: 300, optimalMin: 150, unit: "ng/mL" },
5343
+ kind: "reference-interval",
5743
5344
  source: "tietz-7ed-2015"
5744
5345
  },
5745
5346
  ImmatureGranulocytes: {
5746
5347
  default: { max: 1, min: 0, optimalMax: 0.5, optimalMin: 0, unit: "%" },
5348
+ kind: "reference-interval",
5747
5349
  source: "tietz-7ed-2015"
5748
5350
  },
5749
5351
  Insulin: {
@@ -5755,10 +5357,12 @@ var biomarkerRangeDefinitions = {
5755
5357
  optimalMin: 3,
5756
5358
  unit: "uIU/mL"
5757
5359
  },
5360
+ kind: "reference-interval",
5758
5361
  source: "tietz-7ed-2015"
5759
5362
  },
5760
5363
  Iron: {
5761
5364
  default: { max: 170, min: 60, optimalMax: 140, optimalMin: 80, unit: "mcg/dL" },
5365
+ kind: "reference-interval",
5762
5366
  source: "sbpc-ml-2021",
5763
5367
  variants: [
5764
5368
  {
@@ -5775,6 +5379,7 @@ var biomarkerRangeDefinitions = {
5775
5379
  },
5776
5380
  Lactate: {
5777
5381
  default: { max: 2.2, min: 0.5, optimalMax: 1.8, optimalMin: 0.7, unit: "mmol/L" },
5382
+ kind: "reference-interval",
5778
5383
  source: "tietz-7ed-2015"
5779
5384
  },
5780
5385
  // DHL/LDH — método IFCC a 37 °C (padrão atual na maioria dos laboratórios
@@ -5786,6 +5391,7 @@ var biomarkerRangeDefinitions = {
5786
5391
  LDH: {
5787
5392
  default: { max: 248, min: 0, optimalMax: 248, optimalMin: 0, unit: "U/L" },
5788
5393
  direction: "lower-better",
5394
+ kind: "reference-interval",
5789
5395
  source: "schumann-ifcc-ldh-2002",
5790
5396
  variants: [
5791
5397
  {
@@ -5803,59 +5409,109 @@ var biomarkerRangeDefinitions = {
5803
5409
  LDL: {
5804
5410
  default: { max: 100, min: 0, optimalMax: 70, optimalMin: 0, unit: "mg/dL" },
5805
5411
  direction: "lower-better",
5412
+ kind: "decision-threshold",
5806
5413
  source: "sbc-lipids-2025"
5807
5414
  },
5808
5415
  LDL_Medium: {
5809
5416
  // LDL Medium (LDL Média): lower is better
5810
5417
  // Quest Ion Mobility reference: Male 167-485, Female 121-397 nmol/L, optimal <215
5811
- default: { max: 485, min: 121, optimalMax: 215, optimalMin: 121, unit: "nmol/L" },
5418
+ default: {
5419
+ max: 485,
5420
+ min: 121,
5421
+ minKind: "display",
5422
+ optimalMax: 215,
5423
+ optimalMin: 121,
5424
+ unit: "nmol/L"
5425
+ },
5812
5426
  direction: "lower-better",
5427
+ kind: "reference-interval",
5813
5428
  source: "caulfield-ionmobility-2008"
5814
5429
  },
5815
5430
  LDL_ParticleNumber: {
5816
5431
  // LDL Particle Number: lower is better
5817
5432
  // Quest Ion Mobility reference: 1016-2185 nmol/L, optimal <1138
5818
- default: { max: 2185, min: 1016, optimalMax: 1138, optimalMin: 1016, unit: "nmol/L" },
5433
+ default: {
5434
+ max: 2185,
5435
+ min: 1016,
5436
+ minKind: "display",
5437
+ optimalMax: 1138,
5438
+ optimalMin: 1016,
5439
+ unit: "nmol/L"
5440
+ },
5819
5441
  direction: "lower-better",
5442
+ kind: "reference-interval",
5820
5443
  source: "caulfield-ionmobility-2008"
5821
5444
  },
5822
5445
  LDL_Peak_Size: {
5823
5446
  // LDL Peak Size: higher is better (larger particles less atherogenic)
5824
5447
  // Quest Ion Mobility reference: optimal >222.9 Å (22.29 nm)
5825
- default: { max: 250, min: 217.4, optimalMax: 250, optimalMin: 222.9, unit: "Angstrom" },
5448
+ default: {
5449
+ max: 250,
5450
+ maxKind: "display",
5451
+ min: 217.4,
5452
+ optimalMax: 250,
5453
+ optimalMin: 222.9,
5454
+ unit: "Angstrom"
5455
+ },
5826
5456
  direction: "higher-better",
5457
+ kind: "reference-interval",
5827
5458
  source: "caulfield-ionmobility-2008"
5828
5459
  },
5829
5460
  LDL_Small: {
5830
5461
  // LDL Small (LDL Pequena): lower is better (small dense LDL is most atherogenic)
5831
5462
  // Quest Ion Mobility reference: Male 123-441, Female 126-382 nmol/L, optimal <142
5832
- default: { max: 441, min: 123, optimalMax: 142, optimalMin: 123, unit: "nmol/L" },
5463
+ default: {
5464
+ max: 441,
5465
+ min: 123,
5466
+ minKind: "display",
5467
+ optimalMax: 142,
5468
+ optimalMin: 123,
5469
+ unit: "nmol/L"
5470
+ },
5833
5471
  direction: "lower-better",
5472
+ kind: "reference-interval",
5834
5473
  source: "caulfield-ionmobility-2008"
5835
5474
  },
5836
5475
  Lead: {
5837
5476
  default: { max: 5, min: 0, optimalMax: 2, optimalMin: 0, unit: "mcg/dL" },
5477
+ kind: "decision-threshold",
5838
5478
  source: "nr7-pcmso-2020"
5839
5479
  },
5840
5480
  Leptin: {
5841
- default: { max: 15, min: 2, optimalMax: 12, optimalMin: 3, unit: "ng/mL" },
5481
+ default: { max: 15, min: 2, minKind: "clinical", optimalMax: 12, optimalMin: 3, unit: "ng/mL" },
5842
5482
  direction: "lower-better",
5483
+ kind: "reference-interval",
5843
5484
  source: "tietz-7ed-2015",
5844
5485
  variants: [
5845
5486
  {
5846
5487
  ageMin: 18,
5847
- range: { max: 10, min: 1, optimalMax: 8, optimalMin: 2, unit: "ng/mL" },
5488
+ range: {
5489
+ max: 10,
5490
+ min: 1,
5491
+ minKind: "clinical",
5492
+ optimalMax: 8,
5493
+ optimalMin: 2,
5494
+ unit: "ng/mL"
5495
+ },
5848
5496
  sex: "M"
5849
5497
  },
5850
5498
  {
5851
5499
  ageMin: 18,
5852
- range: { max: 25, min: 4, optimalMax: 18, optimalMin: 5, unit: "ng/mL" },
5500
+ range: {
5501
+ max: 25,
5502
+ min: 4,
5503
+ minKind: "clinical",
5504
+ optimalMax: 18,
5505
+ optimalMin: 5,
5506
+ unit: "ng/mL"
5507
+ },
5853
5508
  sex: "F"
5854
5509
  }
5855
5510
  ]
5856
5511
  },
5857
5512
  LH: {
5858
5513
  default: { max: 8.6, min: 1.7, optimalMax: 8, optimalMin: 2, unit: "mIU/mL" },
5514
+ kind: "reference-interval",
5859
5515
  source: "tietz-7ed-2015",
5860
5516
  variants: [
5861
5517
  {
@@ -5878,6 +5534,7 @@ var biomarkerRangeDefinitions = {
5878
5534
  },
5879
5535
  Omega6_LA: {
5880
5536
  default: { max: 35, min: 15, optimalMax: 28, optimalMin: 18, unit: "%" },
5537
+ kind: "decision-threshold",
5881
5538
  source: "simopoulos-omega-ratio-2002"
5882
5539
  },
5883
5540
  // Lp(a) — SBC 2025 (seção 3.1.1.2.7): níveis ≥ 50 mg/dL ou ≥ 125 nmol/L são
@@ -5889,12 +5546,14 @@ var biomarkerRangeDefinitions = {
5889
5546
  Lipoprotein_a: {
5890
5547
  default: { max: 125, min: 0, optimalMax: 75, optimalMin: 0, unit: "nmol/L" },
5891
5548
  direction: "lower-better",
5549
+ kind: "decision-threshold",
5892
5550
  source: "sbc-lipids-2025"
5893
5551
  },
5894
5552
  // `Lymphocytes` (%) fica sem faixa de propósito e `Lymphocytes_Abs` usa os limites da
5895
5553
  // Tabela 2 da PNS 2019. Ver o comentário em `Neutrophils_Abs`.
5896
5554
  Lymphocytes_Abs: {
5897
5555
  default: { max: 3.414, min: 0.72, unit: "K/uL" },
5556
+ kind: "reference-interval",
5898
5557
  source: "pns-hemograma-2019",
5899
5558
  variants: [
5900
5559
  { ageMin: 18, range: { max: 3.37, min: 0.72, unit: "K/uL" }, sex: "M" },
@@ -5903,40 +5562,49 @@ var biomarkerRangeDefinitions = {
5903
5562
  },
5904
5563
  Magnesium: {
5905
5564
  default: { max: 2.2, min: 1.7, optimalMax: 2.1, optimalMin: 1.9, unit: "mg/dL" },
5565
+ kind: "reference-interval",
5906
5566
  source: "tietz-7ed-2015"
5907
5567
  },
5908
5568
  Magnesium_RBC: {
5909
5569
  default: { max: 6.8, min: 4, optimalMax: 6, optimalMin: 4.5, unit: "mg/dL" },
5570
+ kind: "reference-interval",
5910
5571
  source: "tietz-7ed-2015"
5911
5572
  },
5912
5573
  MCH: {
5913
5574
  default: { max: 33, min: 27, optimalMax: 32, optimalMin: 28, unit: "pg" },
5575
+ kind: "reference-interval",
5914
5576
  source: "pns-hemograma-2019"
5915
5577
  },
5916
5578
  MCHC: {
5917
5579
  default: { max: 36, min: 32, optimalMax: 35, optimalMin: 33, unit: "g/dL" },
5580
+ kind: "reference-interval",
5918
5581
  source: "pns-hemograma-2019"
5919
5582
  },
5920
5583
  MCV: {
5921
5584
  default: { max: 100, min: 80, optimalMax: 98, optimalMin: 82, unit: "fL" },
5585
+ kind: "reference-interval",
5922
5586
  source: "pns-hemograma-2019"
5923
5587
  },
5924
5588
  Mercury: {
5925
5589
  default: { max: 10, min: 0, optimalMax: 5, optimalMin: 0, unit: "mcg/L" },
5590
+ kind: "decision-threshold",
5926
5591
  source: "nr7-pcmso-2020"
5927
5592
  },
5928
5593
  Microalbumin: {
5929
5594
  default: { max: 30, min: 0, optimalMax: 20, optimalMin: 0, unit: "mg/L" },
5595
+ kind: "decision-threshold",
5930
5596
  source: "kdigo-ckd-2024"
5931
5597
  },
5932
5598
  MMA: {
5933
5599
  default: { max: 378, min: 0, optimalMax: 270, optimalMin: 0, unit: "nmol/L" },
5600
+ kind: "reference-interval",
5934
5601
  source: "tietz-7ed-2015"
5935
5602
  },
5936
5603
  // `Monocytes` (%) fica sem faixa de propósito e `Monocytes_Abs` usa os limites da
5937
5604
  // Tabela 2 da PNS 2019. Ver o comentário em `Neutrophils_Abs`.
5938
5605
  Monocytes_Abs: {
5939
5606
  default: { max: 0.812, min: 0.011, unit: "K/uL" },
5607
+ kind: "reference-interval",
5940
5608
  source: "pns-hemograma-2019",
5941
5609
  variants: [
5942
5610
  { ageMin: 18, range: { max: 0.812, min: 0.011, unit: "K/uL" }, sex: "M" },
@@ -5945,11 +5613,13 @@ var biomarkerRangeDefinitions = {
5945
5613
  },
5946
5614
  MPV: {
5947
5615
  default: { max: 11.5, min: 7.5, optimalMax: 10.5, optimalMin: 8, unit: "fL" },
5616
+ kind: "reference-interval",
5948
5617
  source: "pns-hemograma-2019"
5949
5618
  },
5950
5619
  // MPO: Meuwese 2007 (EPIC-Norfolk) — risco CV elevado >322 pmol/L
5951
5620
  Myeloperoxidase: {
5952
5621
  default: { max: 470, min: 0, optimalMax: 322, optimalMin: 0, unit: "pmol/L" },
5622
+ kind: "decision-threshold",
5953
5623
  source: "meuwese-mpo-2007"
5954
5624
  },
5955
5625
  // Leucograma diferencial em percentual (`Neutrophils`, `Lymphocytes`,
@@ -5973,6 +5643,7 @@ var biomarkerRangeDefinitions = {
5973
5643
  // (menor mínimo e maior máximo entre homens e mulheres).
5974
5644
  Neutrophils_Abs: {
5975
5645
  default: { max: 6.474, min: 0.576, unit: "K/uL" },
5646
+ kind: "reference-interval",
5976
5647
  source: "pns-hemograma-2019",
5977
5648
  variants: [
5978
5649
  { ageMin: 18, range: { max: 5.971, min: 0.576, unit: "K/uL" }, sex: "M" },
@@ -5982,22 +5653,27 @@ var biomarkerRangeDefinitions = {
5982
5653
  NonHDL_Cholesterol: {
5983
5654
  default: { max: 130, min: 0, optimalMax: 100, optimalMin: 0, unit: "mg/dL" },
5984
5655
  direction: "lower-better",
5656
+ kind: "decision-threshold",
5985
5657
  source: "sbc-lipids-2025"
5986
5658
  },
5987
5659
  NRBC: {
5988
5660
  default: { max: 0, min: 0, optimalMax: 0, optimalMin: 0, unit: "/100WBC" },
5661
+ kind: "reference-interval",
5989
5662
  source: "tietz-7ed-2015"
5990
5663
  },
5991
5664
  NTproBNP: {
5992
5665
  default: { max: 125, min: 0, optimalMax: 75, optimalMin: 0, unit: "pg/mL" },
5666
+ kind: "decision-threshold",
5993
5667
  source: "sbc-ic-2018"
5994
5668
  },
5995
5669
  Oleic_Acid: {
5996
5670
  default: { max: 25, min: 15, optimalMax: 22, optimalMin: 18, unit: "%" },
5671
+ kind: "reference-interval",
5997
5672
  source: "tietz-7ed-2015"
5998
5673
  },
5999
5674
  Omega3_Index: {
6000
5675
  default: { max: 8, min: 4, optimalMax: 8, optimalMin: 5.5, unit: "%" },
5676
+ kind: "decision-threshold",
6001
5677
  source: "harris-omega3-2004"
6002
5678
  },
6003
5679
  // Ômega-3 Total (% de ácidos graxos eritrocitários) — Harris & von Schacky
@@ -6007,39 +5683,55 @@ var biomarkerRangeDefinitions = {
6007
5683
  // a literatura e o próprio texto educacional do `platform`.
6008
5684
  Omega3_Total: {
6009
5685
  default: { max: 12, min: 3, optimalMax: 10, optimalMin: 8, unit: "%" },
5686
+ kind: "decision-threshold",
6010
5687
  source: "harris-omega3-2004"
6011
5688
  },
6012
5689
  Omega6_Total: {
6013
5690
  default: { max: 40, min: 20, optimalMax: 35, optimalMin: 25, unit: "%" },
5691
+ kind: "decision-threshold",
6014
5692
  source: "simopoulos-omega-ratio-2002"
6015
5693
  },
6016
5694
  Omega6_Omega3_Ratio: {
6017
- default: { max: 10, min: 1, optimalMax: 4, optimalMin: 1, unit: "" },
5695
+ default: {
5696
+ max: 10,
5697
+ min: 1,
5698
+ minKind: "display",
5699
+ optimalMax: 4,
5700
+ optimalMin: 1,
5701
+ unit: ""
5702
+ },
6018
5703
  direction: "lower-better",
5704
+ kind: "decision-threshold",
6019
5705
  source: "simopoulos-omega-ratio-2002"
6020
5706
  },
6021
5707
  OmegaCheck: {
6022
5708
  default: { max: 8, min: 4, optimalMax: 8, optimalMin: 5.5, unit: "%" },
5709
+ kind: "decision-threshold",
6023
5710
  source: "harris-omega3-2004"
6024
5711
  },
6025
5712
  Palmitic_Acid: {
6026
5713
  default: { max: 30, min: 20, optimalMax: 27, optimalMin: 22, unit: "%" },
5714
+ kind: "reference-interval",
6027
5715
  source: "tietz-7ed-2015"
6028
5716
  },
6029
5717
  Phosphorus: {
6030
5718
  default: { max: 4.5, min: 2.5, optimalMax: 4, optimalMin: 3, unit: "mg/dL" },
5719
+ kind: "reference-interval",
6031
5720
  source: "tietz-7ed-2015"
6032
5721
  },
6033
5722
  Platelets: {
6034
5723
  default: { max: 400, min: 150, optimalMax: 350, optimalMin: 180, unit: "K/uL" },
5724
+ kind: "reference-interval",
6035
5725
  source: "pns-hemograma-2019"
6036
5726
  },
6037
5727
  Potassium: {
6038
5728
  default: { max: 5, min: 3.5, optimalMax: 4.6, optimalMin: 3.8, unit: "mEq/L" },
5729
+ kind: "reference-interval",
6039
5730
  source: "tietz-7ed-2015"
6040
5731
  },
6041
5732
  Prealbumin: {
6042
5733
  default: { max: 38, min: 18, optimalMax: 35, optimalMin: 20, unit: "mg/dL" },
5734
+ kind: "reference-interval",
6043
5735
  source: "tietz-7ed-2015"
6044
5736
  },
6045
5737
  // Progesterona — varia drasticamente com a fase do ciclo menstrual.
@@ -6050,6 +5742,7 @@ var biomarkerRangeDefinitions = {
6050
5742
  // ciclo, indisponível neste contexto.
6051
5743
  Progesterone: {
6052
5744
  default: { max: 0.9, min: 0.1, optimalMax: 0.7, optimalMin: 0.2, unit: "ng/mL" },
5745
+ kind: "reference-interval",
6053
5746
  source: "tietz-7ed-2015",
6054
5747
  variants: [
6055
5748
  {
@@ -6076,6 +5769,7 @@ var biomarkerRangeDefinitions = {
6076
5769
  },
6077
5770
  Prolactin: {
6078
5771
  default: { max: 18, min: 2, optimalMax: 15, optimalMin: 4, unit: "ng/mL" },
5772
+ kind: "reference-interval",
6079
5773
  source: "tietz-7ed-2015",
6080
5774
  variants: [
6081
5775
  {
@@ -6092,6 +5786,7 @@ var biomarkerRangeDefinitions = {
6092
5786
  },
6093
5787
  PSA: {
6094
5788
  default: { max: 4, min: 0, optimalMax: 2.5, optimalMin: 0, unit: "ng/mL" },
5789
+ kind: "reference-interval",
6095
5790
  source: "sturgeon-nacb-2008",
6096
5791
  variants: [
6097
5792
  {
@@ -6121,6 +5816,7 @@ var biomarkerRangeDefinitions = {
6121
5816
  },
6122
5817
  RBC: {
6123
5818
  default: { max: 5.5, min: 4, optimalMax: 5.2, optimalMin: 4.3, unit: "M/uL" },
5819
+ kind: "reference-interval",
6124
5820
  source: "pns-hemograma-2019",
6125
5821
  variants: [
6126
5822
  {
@@ -6137,22 +5833,27 @@ var biomarkerRangeDefinitions = {
6137
5833
  },
6138
5834
  RDW: {
6139
5835
  default: { max: 14.5, min: 11.5, optimalMax: 14, optimalMin: 12, unit: "%" },
5836
+ kind: "reference-interval",
6140
5837
  source: "pns-hemograma-2019"
6141
5838
  },
6142
5839
  Reticulocytes: {
6143
5840
  default: { max: 2.5, min: 0.5, optimalMax: 2, optimalMin: 0.8, unit: "%" },
5841
+ kind: "reference-interval",
6144
5842
  source: "pns-hemograma-2019"
6145
5843
  },
6146
5844
  SDMA: {
6147
5845
  default: { max: 0.6, min: 0.3, optimalMax: 0.5, optimalMin: 0.3, unit: "umol/L" },
5846
+ kind: "reference-interval",
6148
5847
  source: "schwedhelm-sdma-2011"
6149
5848
  },
6150
5849
  Selenium: {
6151
5850
  default: { max: 150, min: 70, optimalMax: 125, optimalMin: 85, unit: "\xB5g/L" },
5851
+ kind: "reference-interval",
6152
5852
  source: "tietz-7ed-2015"
6153
5853
  },
6154
5854
  SHBG: {
6155
5855
  default: { max: 54, min: 18, optimalMax: 50, optimalMin: 20, unit: "nmol/L" },
5856
+ kind: "reference-interval",
6156
5857
  source: "tietz-7ed-2015",
6157
5858
  variants: [
6158
5859
  {
@@ -6169,34 +5870,42 @@ var biomarkerRangeDefinitions = {
6169
5870
  },
6170
5871
  Sodium: {
6171
5872
  default: { max: 145, min: 136, optimalMax: 143, optimalMin: 138, unit: "mEq/L" },
5873
+ kind: "reference-interval",
6172
5874
  source: "tietz-7ed-2015"
6173
5875
  },
6174
5876
  SpecificGravity_Urine: {
6175
5877
  default: { max: 1.03, min: 1.005, optimalMax: 1.025, optimalMin: 1.01, unit: "SG" },
5878
+ kind: "reference-interval",
6176
5879
  source: "tietz-7ed-2015"
6177
5880
  },
6178
5881
  Stearic_Acid: {
6179
5882
  default: { max: 14, min: 8, optimalMax: 12, optimalMin: 10, unit: "%" },
5883
+ kind: "reference-interval",
6180
5884
  source: "tietz-7ed-2015"
6181
5885
  },
6182
5886
  T3Free: {
6183
5887
  default: { max: 4.2, min: 2.3, optimalMax: 3.8, optimalMin: 2.8, unit: "pg/mL" },
5888
+ kind: "reference-interval",
6184
5889
  source: "tietz-7ed-2015"
6185
5890
  },
6186
5891
  T3Reverse: {
6187
5892
  default: { max: 24, min: 10, optimalMax: 20, optimalMin: 12, unit: "ng/dL" },
5893
+ kind: "reference-interval",
6188
5894
  source: "tietz-7ed-2015"
6189
5895
  },
6190
5896
  T4Free: {
6191
5897
  default: { max: 1.8, min: 0.8, optimalMax: 1.5, optimalMin: 1, unit: "ng/dL" },
5898
+ kind: "reference-interval",
6192
5899
  source: "tietz-7ed-2015"
6193
5900
  },
6194
5901
  T4Total: {
6195
5902
  default: { max: 12, min: 4.5, optimalMax: 10, optimalMin: 6, unit: "ug/dL" },
5903
+ kind: "reference-interval",
6196
5904
  source: "tietz-7ed-2015"
6197
5905
  },
6198
5906
  Testosterone: {
6199
5907
  default: { max: 1e3, min: 300, optimalMax: 800, optimalMin: 500, unit: "ng/dL" },
5908
+ kind: "reference-interval",
6200
5909
  source: "tietz-7ed-2015",
6201
5910
  variants: [
6202
5911
  {
@@ -6219,12 +5928,14 @@ var biomarkerRangeDefinitions = {
6219
5928
  },
6220
5929
  TestosteroneBioavailable: {
6221
5930
  default: { max: 200, min: 50, optimalMax: 150, optimalMin: 80, unit: "ng/dL" },
5931
+ kind: "reference-interval",
6222
5932
  source: "tietz-7ed-2015"
6223
5933
  },
6224
5934
  // TestosteroneFree - using direct immunoassay ranges (Quest/LabCorp standard)
6225
5935
  // Previous values (9.3-26.5 pg/mL) were based on equilibrium dialysis method
6226
5936
  TestosteroneFree: {
6227
5937
  default: { max: 155, min: 35, optimalMax: 120, optimalMin: 50, unit: "pg/mL" },
5938
+ kind: "reference-interval",
6228
5939
  source: "tietz-7ed-2015",
6229
5940
  variants: [
6230
5941
  {
@@ -6247,22 +5958,27 @@ var biomarkerRangeDefinitions = {
6247
5958
  },
6248
5959
  TIBC: {
6249
5960
  default: { max: 370, min: 250, optimalMax: 350, optimalMin: 280, unit: "mcg/dL" },
5961
+ kind: "reference-interval",
6250
5962
  source: "tietz-7ed-2015"
6251
5963
  },
6252
5964
  TotalProtein: {
6253
5965
  default: { max: 8.3, min: 6, optimalMax: 7.8, optimalMin: 6.5, unit: "g/dL" },
5966
+ kind: "reference-interval",
6254
5967
  source: "tietz-7ed-2015"
6255
5968
  },
6256
5969
  Trans_Fat_Index: {
6257
5970
  default: { max: 1, min: 0, optimalMax: 0.5, optimalMin: 0, unit: "%" },
5971
+ kind: "reference-interval",
6258
5972
  source: "tietz-7ed-2015"
6259
5973
  },
6260
5974
  Transferrin: {
6261
5975
  default: { max: 360, min: 200, optimalMax: 340, optimalMin: 220, unit: "mg/dL" },
5976
+ kind: "reference-interval",
6262
5977
  source: "tietz-7ed-2015"
6263
5978
  },
6264
5979
  TransferrinSaturation: {
6265
5980
  default: { max: 50, min: 20, optimalMax: 45, optimalMin: 25, unit: "%" },
5981
+ kind: "reference-interval",
6266
5982
  source: "tietz-7ed-2015",
6267
5983
  variants: [
6268
5984
  {
@@ -6290,16 +6006,19 @@ var biomarkerRangeDefinitions = {
6290
6006
  unit: "mg/dL"
6291
6007
  },
6292
6008
  direction: "lower-better",
6009
+ kind: "decision-threshold",
6293
6010
  source: "sbc-lipids-2025"
6294
6011
  },
6295
6012
  // TroponinI: 0.04 ng/mL = percentil 99 (ensaio Siemens TnI-Ultra)
6296
6013
  TroponinI: {
6297
6014
  default: { max: 0.04, min: 0, optimalMax: 0.02, optimalMin: 0, unit: "ng/mL" },
6015
+ kind: "reference-interval",
6298
6016
  source: "keller-tni-2013"
6299
6017
  },
6300
6018
  // TroponinT: 14 ng/L = percentil 99 hs-cTnT (ensaio Roche Elecsys 5ª geração)
6301
6019
  TroponinT: {
6302
6020
  default: { max: 14, min: 0, optimalMax: 10, optimalMin: 0, unit: "ng/L" },
6021
+ kind: "reference-interval",
6303
6022
  source: "giannitsis-hstnt-2010"
6304
6023
  },
6305
6024
  // TSH — 0.4–4.0 é a faixa de referência adulta geral. O alvo 2.5 µIU/mL é
@@ -6309,6 +6028,7 @@ var biomarkerRangeDefinitions = {
6309
6028
  // expandido (tolerância fisiológica do eixo em idosos, SBEM 2013).
6310
6029
  TSH: {
6311
6030
  default: { max: 4, min: 0.4, optimalMax: 3, optimalMin: 1, unit: "uIU/mL" },
6031
+ kind: "reference-interval",
6312
6032
  source: "sbem-thyroid-2013",
6313
6033
  variants: [
6314
6034
  // Variantes gestacionais (ATA 2017 / SBEM): supressão fisiológica por hCG
@@ -6357,10 +6077,12 @@ var biomarkerRangeDefinitions = {
6357
6077
  // deve ser revisto em biomarkers.ts — ver issue #41.
6358
6078
  Urea: {
6359
6079
  default: { max: 50, min: 15, optimalMax: 40, optimalMin: 20, unit: "mg/dL" },
6080
+ kind: "reference-interval",
6360
6081
  source: "tietz-7ed-2015"
6361
6082
  },
6362
6083
  UricAcid: {
6363
6084
  default: { max: 7, min: 2.5, optimalMax: 5.5, optimalMin: 3, unit: "mg/dL" },
6085
+ kind: "reference-interval",
6364
6086
  source: "tietz-7ed-2015",
6365
6087
  variants: [
6366
6088
  {
@@ -6377,50 +6099,62 @@ var biomarkerRangeDefinitions = {
6377
6099
  },
6378
6100
  Urobilinogen_Urine: {
6379
6101
  default: { max: 1, min: 0.1, optimalMax: 1, optimalMin: 0.1, unit: "mg/dL" },
6102
+ kind: "reference-interval",
6380
6103
  source: "tietz-7ed-2015"
6381
6104
  },
6382
6105
  pH_Urine: {
6383
6106
  default: { max: 8, min: 4.5, optimalMax: 7, optimalMin: 5.5, unit: "pH" },
6107
+ kind: "reference-interval",
6384
6108
  source: "tietz-7ed-2015"
6385
6109
  },
6386
6110
  HyalineCasts_Urine: {
6387
6111
  default: { max: 2, min: 0, optimalMax: 1, optimalMin: 0, unit: "/LPF" },
6112
+ kind: "reference-interval",
6388
6113
  source: "tietz-7ed-2015"
6389
6114
  },
6390
6115
  RBC_Urine: {
6391
6116
  default: { max: 3, min: 0, optimalMax: 1, optimalMin: 0, unit: "/HPF" },
6117
+ kind: "reference-interval",
6392
6118
  source: "tietz-7ed-2015"
6393
6119
  },
6394
6120
  SquamousEpithelial_Urine: {
6395
6121
  default: { max: 15, min: 0, optimalMax: 5, optimalMin: 0, unit: "/HPF" },
6122
+ kind: "reference-interval",
6396
6123
  source: "tietz-7ed-2015"
6397
6124
  },
6398
6125
  Leukocytes_Urine: {
6399
6126
  default: { max: 5, min: 0, optimalMax: 2, optimalMin: 0, unit: "/HPF" },
6127
+ kind: "reference-interval",
6400
6128
  source: "tietz-7ed-2015"
6401
6129
  },
6402
6130
  Folate: {
6403
6131
  default: { max: 20, min: 3.9, optimalMax: 17, optimalMin: 5, unit: "ng/mL" },
6132
+ kind: "reference-interval",
6404
6133
  source: "tietz-7ed-2015"
6405
6134
  },
6406
6135
  VitaminA: {
6407
6136
  default: { max: 100, min: 20, optimalMax: 80, optimalMin: 30, unit: "mcg/dL" },
6137
+ kind: "reference-interval",
6408
6138
  source: "tietz-7ed-2015"
6409
6139
  },
6410
6140
  VitaminB1: {
6411
6141
  default: { max: 180, min: 70, optimalMax: 150, optimalMin: 80, unit: "nmol/L" },
6142
+ kind: "reference-interval",
6412
6143
  source: "tietz-7ed-2015"
6413
6144
  },
6414
6145
  VitaminB12: {
6415
6146
  default: { max: 900, min: 200, optimalMax: 800, optimalMin: 400, unit: "pg/mL" },
6147
+ kind: "reference-interval",
6416
6148
  source: "tietz-7ed-2015"
6417
6149
  },
6418
6150
  VitaminB6: {
6419
6151
  default: { max: 50, min: 5, optimalMax: 40, optimalMin: 10, unit: "ng/mL" },
6152
+ kind: "reference-interval",
6420
6153
  source: "tietz-7ed-2015"
6421
6154
  },
6422
6155
  VitaminC: {
6423
6156
  default: { max: 2, min: 0.4, optimalMax: 1.5, optimalMin: 0.6, unit: "mg/dL" },
6157
+ kind: "reference-interval",
6424
6158
  source: "tietz-7ed-2015"
6425
6159
  },
6426
6160
  // Vitamina D — posicionamento conjunto SBEM/SBPC-ML 2017:
@@ -6437,6 +6171,7 @@ var biomarkerRangeDefinitions = {
6437
6171
  // quando a modelagem de comorbidades chegar ao schema.
6438
6172
  VitaminD: {
6439
6173
  default: { max: 100, min: 20, optimalMax: 70, optimalMin: 40, unit: "ng/mL" },
6174
+ kind: "decision-threshold",
6440
6175
  source: "ferreira-vitd-2017",
6441
6176
  variants: [
6442
6177
  {
@@ -6448,10 +6183,12 @@ var biomarkerRangeDefinitions = {
6448
6183
  },
6449
6184
  VitaminD_1_25: {
6450
6185
  default: { max: 72, min: 18, optimalMax: 60, optimalMin: 25, unit: "pg/mL" },
6186
+ kind: "reference-interval",
6451
6187
  source: "tietz-7ed-2015"
6452
6188
  },
6453
6189
  VitaminE: {
6454
6190
  default: { max: 17, min: 5.5, optimalMax: 14, optimalMin: 7, unit: "mg/L" },
6191
+ kind: "reference-interval",
6455
6192
  source: "tietz-7ed-2015"
6456
6193
  },
6457
6194
  // VLDL — estimado clinicamente via TG/5 (método Friedewald). O corte de
@@ -6460,30 +6197,34 @@ var biomarkerRangeDefinitions = {
6460
6197
  // método de cálculo, mas a faixa de referência tem respaldo na diretriz
6461
6198
  // SBC 2025.
6462
6199
  VLDL: {
6463
- default: { max: 30, min: 2, optimalMax: 20, optimalMin: 5, unit: "mg/dL" },
6200
+ default: { max: 30, min: 2, minKind: "display", optimalMax: 20, optimalMin: 5, unit: "mg/dL" },
6201
+ kind: "decision-threshold",
6464
6202
  source: "sbc-lipids-2025"
6465
6203
  },
6466
6204
  WBC: {
6467
6205
  default: { max: 11, min: 4, optimalMax: 8, optimalMin: 5, unit: "K/uL" },
6206
+ kind: "reference-interval",
6468
6207
  source: "pns-hemograma-2019"
6469
6208
  },
6470
6209
  Zinc: {
6471
6210
  default: { max: 120, min: 60, optimalMax: 100, optimalMin: 70, unit: "mcg/dL" },
6211
+ kind: "reference-interval",
6472
6212
  source: "tietz-7ed-2015"
6473
6213
  },
6474
6214
  AndroidFatPct: {
6475
- default: { max: 35, min: 10, optimalMax: 25, optimalMin: 15, unit: "%" },
6215
+ default: { max: 35, min: 10, minKind: "display", optimalMax: 25, optimalMin: 15, unit: "%" },
6476
6216
  direction: "lower-better",
6217
+ kind: "population",
6477
6218
  source: "kelly-dxa-2009",
6478
6219
  variants: [
6479
6220
  {
6480
6221
  ageMin: 18,
6481
- range: { max: 30, min: 10, optimalMax: 22, optimalMin: 12, unit: "%" },
6222
+ range: { max: 30, min: 10, minKind: "display", optimalMax: 22, optimalMin: 12, unit: "%" },
6482
6223
  sex: "M"
6483
6224
  },
6484
6225
  {
6485
6226
  ageMin: 18,
6486
- range: { max: 42, min: 15, optimalMax: 32, optimalMin: 20, unit: "%" },
6227
+ range: { max: 42, min: 15, minKind: "display", optimalMax: 32, optimalMin: 20, unit: "%" },
6487
6228
  sex: "F"
6488
6229
  }
6489
6230
  ]
@@ -6534,6 +6275,7 @@ var biomarkerRangeDefinitions = {
6534
6275
  CalfCircumference: {
6535
6276
  default: { unit: "cm" },
6536
6277
  direction: "higher-better",
6278
+ kind: "decision-threshold",
6537
6279
  source: "ewgsop2-2019",
6538
6280
  // O corte não é específico por sexo na fonte; as duas variantes existem
6539
6281
  // porque `RangeVariant` exige o campo, e carregam o mesmo número.
@@ -6545,38 +6287,76 @@ var biomarkerRangeDefinitions = {
6545
6287
  WaistToHeightRatio: {
6546
6288
  default: { max: 0.5, unit: "" },
6547
6289
  direction: "lower-better",
6290
+ kind: "decision-threshold",
6548
6291
  source: "browning-ashwell-2010"
6549
6292
  },
6550
6293
  AndroidGynoidRatio: {
6551
- default: { max: 1.2, min: 0.5, optimalMax: 1, optimalMin: 0.6, unit: "" },
6294
+ default: { max: 1.2, min: 0.5, minKind: "display", optimalMax: 1, optimalMin: 0.6, unit: "" },
6552
6295
  direction: "lower-better",
6296
+ kind: "population",
6553
6297
  source: "kelly-dxa-2009",
6554
6298
  variants: [
6555
6299
  {
6556
6300
  ageMin: 18,
6557
- range: { max: 1.2, min: 0.6, optimalMax: 1, optimalMin: 0.7, unit: "" },
6301
+ range: {
6302
+ max: 1.2,
6303
+ min: 0.6,
6304
+ minKind: "display",
6305
+ optimalMax: 1,
6306
+ optimalMin: 0.7,
6307
+ unit: ""
6308
+ },
6558
6309
  sex: "M"
6559
6310
  },
6560
6311
  {
6561
6312
  ageMin: 18,
6562
- range: { max: 1, min: 0.4, optimalMax: 0.8, optimalMin: 0.5, unit: "" },
6313
+ range: {
6314
+ max: 1,
6315
+ min: 0.4,
6316
+ minKind: "display",
6317
+ optimalMax: 0.8,
6318
+ optimalMin: 0.5,
6319
+ unit: ""
6320
+ },
6563
6321
  sex: "F"
6564
6322
  }
6565
6323
  ]
6566
6324
  },
6567
6325
  BMC: {
6568
- default: { max: 3.5, min: 2, optimalMax: 3.2, optimalMin: 2.3, unit: "kg" },
6326
+ default: {
6327
+ max: 3.5,
6328
+ maxKind: "display",
6329
+ min: 2,
6330
+ optimalMax: 3.2,
6331
+ optimalMin: 2.3,
6332
+ unit: "kg"
6333
+ },
6569
6334
  direction: "higher-better",
6335
+ kind: "population",
6570
6336
  source: "kelly-dxa-2009",
6571
6337
  variants: [
6572
6338
  {
6573
6339
  ageMin: 18,
6574
- range: { max: 4, min: 2.5, optimalMax: 3.5, optimalMin: 2.8, unit: "kg" },
6340
+ range: {
6341
+ max: 4,
6342
+ maxKind: "display",
6343
+ min: 2.5,
6344
+ optimalMax: 3.5,
6345
+ optimalMin: 2.8,
6346
+ unit: "kg"
6347
+ },
6575
6348
  sex: "M"
6576
6349
  },
6577
6350
  {
6578
6351
  ageMin: 18,
6579
- range: { max: 3, min: 1.8, optimalMax: 2.7, optimalMin: 2, unit: "kg" },
6352
+ range: {
6353
+ max: 3,
6354
+ maxKind: "display",
6355
+ min: 1.8,
6356
+ optimalMax: 2.7,
6357
+ optimalMin: 2,
6358
+ unit: "kg"
6359
+ },
6580
6360
  sex: "F"
6581
6361
  }
6582
6362
  ]
@@ -6584,142 +6364,148 @@ var biomarkerRangeDefinitions = {
6584
6364
  // BodyFatPct — Age-bracketed ranges from Gallagher et al. Am J Clin Nutr 2000;72:694-701
6585
6365
  // (PMID: 10966886) and ACSM Guidelines for Exercise Testing, 11th Ed (2021)
6586
6366
  BodyFatPct: {
6587
- default: { max: 30, min: 10, optimalMax: 25, optimalMin: 15, unit: "%" },
6367
+ default: { max: 30, min: 10, minKind: "clinical", optimalMax: 25, optimalMin: 15, unit: "%" },
6588
6368
  direction: "lower-better",
6369
+ kind: "decision-threshold",
6589
6370
  source: "gallagher-bodyfat-2000",
6590
6371
  variants: [
6591
6372
  // Men — age brackets
6592
6373
  {
6593
6374
  ageMax: 25,
6594
6375
  ageMin: 18,
6595
- range: { max: 25, min: 5, optimalMax: 20, optimalMin: 10, unit: "%" },
6376
+ range: { max: 25, min: 5, minKind: "clinical", optimalMax: 20, optimalMin: 10, unit: "%" },
6596
6377
  sex: "M"
6597
6378
  },
6598
6379
  {
6599
6380
  ageMax: 35,
6600
6381
  ageMin: 26,
6601
- range: { max: 26, min: 5, optimalMax: 21, optimalMin: 11, unit: "%" },
6382
+ range: { max: 26, min: 5, minKind: "clinical", optimalMax: 21, optimalMin: 11, unit: "%" },
6602
6383
  sex: "M"
6603
6384
  },
6604
6385
  {
6605
6386
  ageMax: 45,
6606
6387
  ageMin: 36,
6607
- range: { max: 27, min: 5, optimalMax: 22, optimalMin: 12, unit: "%" },
6388
+ range: { max: 27, min: 5, minKind: "clinical", optimalMax: 22, optimalMin: 12, unit: "%" },
6608
6389
  sex: "M"
6609
6390
  },
6610
6391
  {
6611
6392
  ageMax: 55,
6612
6393
  ageMin: 46,
6613
- range: { max: 28, min: 5, optimalMax: 23, optimalMin: 13, unit: "%" },
6394
+ range: { max: 28, min: 5, minKind: "clinical", optimalMax: 23, optimalMin: 13, unit: "%" },
6614
6395
  sex: "M"
6615
6396
  },
6616
6397
  {
6617
6398
  ageMin: 56,
6618
- range: { max: 29, min: 5, optimalMax: 24, optimalMin: 14, unit: "%" },
6399
+ range: { max: 29, min: 5, minKind: "clinical", optimalMax: 24, optimalMin: 14, unit: "%" },
6619
6400
  sex: "M"
6620
6401
  },
6621
6402
  // Women — age brackets
6622
6403
  {
6623
6404
  ageMax: 25,
6624
6405
  ageMin: 18,
6625
- range: { max: 32, min: 13, optimalMax: 28, optimalMin: 18, unit: "%" },
6406
+ range: { max: 32, min: 13, minKind: "clinical", optimalMax: 28, optimalMin: 18, unit: "%" },
6626
6407
  sex: "F"
6627
6408
  },
6628
6409
  {
6629
6410
  ageMax: 35,
6630
6411
  ageMin: 26,
6631
- range: { max: 33, min: 13, optimalMax: 29, optimalMin: 18, unit: "%" },
6412
+ range: { max: 33, min: 13, minKind: "clinical", optimalMax: 29, optimalMin: 18, unit: "%" },
6632
6413
  sex: "F"
6633
6414
  },
6634
6415
  {
6635
6416
  ageMax: 45,
6636
6417
  ageMin: 36,
6637
- range: { max: 34, min: 13, optimalMax: 30, optimalMin: 19, unit: "%" },
6418
+ range: { max: 34, min: 13, minKind: "clinical", optimalMax: 30, optimalMin: 19, unit: "%" },
6638
6419
  sex: "F"
6639
6420
  },
6640
6421
  {
6641
6422
  ageMax: 55,
6642
6423
  ageMin: 46,
6643
- range: { max: 35, min: 13, optimalMax: 31, optimalMin: 20, unit: "%" },
6424
+ range: { max: 35, min: 13, minKind: "clinical", optimalMax: 31, optimalMin: 20, unit: "%" },
6644
6425
  sex: "F"
6645
6426
  },
6646
6427
  {
6647
6428
  ageMin: 56,
6648
- range: { max: 36, min: 13, optimalMax: 32, optimalMin: 20, unit: "%" },
6429
+ range: { max: 36, min: 13, minKind: "clinical", optimalMax: 32, optimalMin: 20, unit: "%" },
6649
6430
  sex: "F"
6650
6431
  }
6651
6432
  ]
6652
6433
  },
6653
6434
  FatFreeMass: {
6654
- default: { max: 80, min: 40, optimalMax: 70, optimalMin: 45, unit: "kg" },
6435
+ default: { max: 80, maxKind: "display", min: 40, optimalMax: 70, optimalMin: 45, unit: "kg" },
6655
6436
  direction: "higher-better",
6437
+ kind: "population",
6656
6438
  source: "kelly-dxa-2009",
6657
6439
  variants: [
6658
6440
  {
6659
6441
  ageMin: 18,
6660
- range: { max: 90, min: 50, optimalMax: 80, optimalMin: 55, unit: "kg" },
6442
+ range: { max: 90, maxKind: "display", min: 50, optimalMax: 80, optimalMin: 55, unit: "kg" },
6661
6443
  sex: "M"
6662
6444
  },
6663
6445
  {
6664
6446
  ageMin: 18,
6665
- range: { max: 60, min: 35, optimalMax: 52, optimalMin: 38, unit: "kg" },
6447
+ range: { max: 60, maxKind: "display", min: 35, optimalMax: 52, optimalMin: 38, unit: "kg" },
6666
6448
  sex: "F"
6667
6449
  }
6668
6450
  ]
6669
6451
  },
6670
6452
  FatMass: {
6671
- default: { max: 30, min: 5, optimalMax: 20, optimalMin: 8, unit: "kg" },
6453
+ default: { max: 30, min: 5, minKind: "clinical", optimalMax: 20, optimalMin: 8, unit: "kg" },
6672
6454
  direction: "lower-better",
6455
+ kind: "population",
6673
6456
  source: "kelly-dxa-2009",
6674
6457
  variants: [
6675
6458
  {
6676
6459
  ageMin: 18,
6677
- range: { max: 25, min: 5, optimalMax: 18, optimalMin: 7, unit: "kg" },
6460
+ range: { max: 25, min: 5, minKind: "clinical", optimalMax: 18, optimalMin: 7, unit: "kg" },
6678
6461
  sex: "M"
6679
6462
  },
6680
6463
  {
6681
6464
  ageMin: 18,
6682
- range: { max: 30, min: 8, optimalMax: 22, optimalMin: 10, unit: "kg" },
6465
+ range: { max: 30, min: 8, minKind: "clinical", optimalMax: 22, optimalMin: 10, unit: "kg" },
6683
6466
  sex: "F"
6684
6467
  }
6685
6468
  ]
6686
6469
  },
6687
6470
  GynoidFatPct: {
6688
- default: { max: 45, min: 15, optimalMax: 35, optimalMin: 20, unit: "%" },
6471
+ default: { max: 45, min: 15, minKind: "display", optimalMax: 35, optimalMin: 20, unit: "%" },
6689
6472
  direction: "lower-better",
6473
+ kind: "population",
6690
6474
  source: "kelly-dxa-2009",
6691
6475
  variants: [
6692
6476
  {
6693
6477
  ageMin: 18,
6694
- range: { max: 35, min: 12, optimalMax: 28, optimalMin: 15, unit: "%" },
6478
+ range: { max: 35, min: 12, minKind: "display", optimalMax: 28, optimalMin: 15, unit: "%" },
6695
6479
  sex: "M"
6696
6480
  },
6697
6481
  {
6698
6482
  ageMin: 18,
6699
- range: { max: 50, min: 20, optimalMax: 40, optimalMin: 25, unit: "%" },
6483
+ range: { max: 50, min: 20, minKind: "display", optimalMax: 40, optimalMin: 25, unit: "%" },
6700
6484
  sex: "F"
6701
6485
  }
6702
6486
  ]
6703
6487
  },
6704
6488
  LeanMass: {
6705
- default: { max: 75, min: 35, optimalMax: 65, optimalMin: 40, unit: "kg" },
6489
+ default: { max: 75, maxKind: "display", min: 35, optimalMax: 65, optimalMin: 40, unit: "kg" },
6706
6490
  direction: "higher-better",
6491
+ kind: "population",
6707
6492
  source: "kelly-dxa-2009",
6708
6493
  variants: [
6709
6494
  {
6710
6495
  ageMin: 18,
6711
- range: { max: 85, min: 45, optimalMax: 75, optimalMin: 50, unit: "kg" },
6496
+ range: { max: 85, maxKind: "display", min: 45, optimalMax: 75, optimalMin: 50, unit: "kg" },
6712
6497
  sex: "M"
6713
6498
  },
6714
6499
  {
6715
6500
  ageMin: 18,
6716
- range: { max: 55, min: 30, optimalMax: 48, optimalMin: 35, unit: "kg" },
6501
+ range: { max: 55, maxKind: "display", min: 30, optimalMax: 48, optimalMin: 35, unit: "kg" },
6717
6502
  sex: "F"
6718
6503
  }
6719
6504
  ]
6720
6505
  },
6721
6506
  TotalMass: {
6722
6507
  default: { max: 100, min: 50, optimalMax: 85, optimalMin: 55, unit: "kg" },
6508
+ kind: "population",
6723
6509
  source: "kelly-dxa-2009",
6724
6510
  variants: [
6725
6511
  {
@@ -6734,226 +6520,858 @@ var biomarkerRangeDefinitions = {
6734
6520
  }
6735
6521
  ]
6736
6522
  },
6737
- VATMass: {
6738
- default: { max: 1.5, min: 0, optimalMax: 0.8, optimalMin: 0, unit: "kg" },
6739
- direction: "lower-better",
6740
- source: "ofenheimer-vat-2020",
6523
+ VATMass: {
6524
+ default: { max: 1.5, min: 0, optimalMax: 0.8, optimalMin: 0, unit: "kg" },
6525
+ direction: "lower-better",
6526
+ kind: "population",
6527
+ source: "ofenheimer-vat-2020",
6528
+ variants: [
6529
+ {
6530
+ ageMin: 18,
6531
+ range: { max: 1.5, min: 0, optimalMax: 1, optimalMin: 0, unit: "kg" },
6532
+ sex: "M"
6533
+ },
6534
+ {
6535
+ ageMin: 18,
6536
+ range: { max: 1, min: 0, optimalMax: 0.5, optimalMin: 0, unit: "kg" },
6537
+ sex: "F"
6538
+ }
6539
+ ]
6540
+ },
6541
+ VATVolume: {
6542
+ default: {
6543
+ max: 852,
6544
+ min: 0,
6545
+ optimalMax: 600,
6546
+ optimalMin: 0,
6547
+ unit: "cm\xB3",
6548
+ warningMax: 1837
6549
+ },
6550
+ direction: "lower-better",
6551
+ kind: "decision-threshold",
6552
+ source: "ge-corescan"
6553
+ },
6554
+ BMD_Total: {
6555
+ default: {
6556
+ max: 1.4,
6557
+ maxKind: "display",
6558
+ min: 0.9,
6559
+ optimalMax: 1.3,
6560
+ optimalMin: 1,
6561
+ unit: "g/cm\xB2"
6562
+ },
6563
+ direction: "higher-better",
6564
+ kind: "decision-threshold",
6565
+ source: "who-osteoporosis-1994"
6566
+ },
6567
+ TScore_Total: {
6568
+ default: {
6569
+ max: 4,
6570
+ maxKind: "display",
6571
+ min: -1,
6572
+ optimalMax: 2,
6573
+ optimalMin: -0.5,
6574
+ unit: ""
6575
+ },
6576
+ direction: "higher-better",
6577
+ kind: "decision-threshold",
6578
+ source: "who-osteoporosis-1994"
6579
+ },
6580
+ ZScore_Total: {
6581
+ default: {
6582
+ max: 2,
6583
+ maxKind: "display",
6584
+ min: -2,
6585
+ optimalMax: 1,
6586
+ optimalMin: -1,
6587
+ unit: ""
6588
+ },
6589
+ direction: "higher-better",
6590
+ kind: "decision-threshold",
6591
+ source: "who-osteoporosis-1994"
6592
+ },
6593
+ Amylase: {
6594
+ default: { max: 100, min: 28, optimalMax: 90, optimalMin: 35, unit: "U/L" },
6595
+ kind: "reference-interval",
6596
+ source: "tietz-7ed-2015"
6597
+ },
6598
+ Creatinine_Urine: {
6599
+ default: { max: 300, min: 20, optimalMax: 250, optimalMin: 40, unit: "mg/dL" },
6600
+ kind: "reference-interval",
6601
+ source: "tietz-7ed-2015",
6602
+ variants: [
6603
+ {
6604
+ ageMin: 18,
6605
+ range: { max: 300, min: 40, optimalMax: 260, optimalMin: 60, unit: "mg/dL" },
6606
+ sex: "M"
6607
+ },
6608
+ {
6609
+ ageMin: 18,
6610
+ range: { max: 220, min: 20, optimalMax: 180, optimalMin: 40, unit: "mg/dL" },
6611
+ sex: "F"
6612
+ }
6613
+ ]
6614
+ },
6615
+ IgA: {
6616
+ default: { max: 400, min: 70, optimalMax: 350, optimalMin: 100, unit: "mg/dL" },
6617
+ kind: "reference-interval",
6618
+ source: "tietz-7ed-2015"
6619
+ },
6620
+ Lipase: {
6621
+ default: { max: 60, min: 0, optimalMax: 50, optimalMin: 10, unit: "U/L" },
6622
+ kind: "reference-interval",
6623
+ source: "tietz-7ed-2015"
6624
+ },
6625
+ Microalbumin_Urine: {
6626
+ default: { max: 30, min: 0, optimalMax: 20, optimalMin: 0, unit: "mg/L" },
6627
+ kind: "decision-threshold",
6628
+ source: "kdigo-ckd-2024"
6629
+ },
6630
+ PSA_Free: {
6631
+ default: { max: 1.5, min: 0, optimalMax: 1, optimalMin: 0, unit: "ng/mL" },
6632
+ kind: "reference-interval",
6633
+ source: "sturgeon-nacb-2008"
6634
+ },
6635
+ // Relação PSA livre/total — `higher-better`: razão maior = menor risco de
6636
+ // câncer de próstata, logo não há limite superior "ruim" (acima do normal é
6637
+ // verde). Faixas alinhadas à tabela clínica: ≥25% baixo risco (verde),
6638
+ // 10–25% atenção — intermediário/moderado (âmbar), <10% alto risco >50%
6639
+ // (vermelho). Limites: `min` (25) é o piso do verde e `warningMin` (10) o
6640
+ // piso do âmbar — o vermelho é estritamente abaixo de 10% (< 10%); 10%
6641
+ // pertence ao âmbar, conforme a faixa "10–15% moderado" da tabela.
6642
+ PSA_FreeRatio: {
6643
+ default: {
6644
+ max: 100,
6645
+ maxKind: "display",
6646
+ min: 25,
6647
+ optimalMax: 100,
6648
+ optimalMin: 30,
6649
+ unit: "%",
6650
+ warningMin: 10
6651
+ },
6652
+ direction: "higher-better",
6653
+ kind: "decision-threshold",
6654
+ source: "sturgeon-nacb-2008"
6655
+ },
6656
+ // BMI - WHO classification: 18.5–24.9 normal, 25–29.9 overweight, ≥30 obese
6657
+ BMI: {
6658
+ default: { max: 30, min: 18.5, optimalMax: 24.9, optimalMin: 18.5, unit: "kg/m2" },
6659
+ kind: "decision-threshold",
6660
+ source: "who-obesity-2000"
6661
+ },
6662
+ // eAG — Estimated Average Glucose, derivada da HbA1c pela fórmula ADAG:
6663
+ // eAG (mg/dL) = 28,7 × HbA1c(%) − 46,7 (NATHAN, D. M. et al. Translating the
6664
+ // A1C assay into estimated average glucose values. Diabetes Care, v. 31, n. 8,
6665
+ // p. 1473-1478, 2008. DOI: 10.2337/dc08-0545).
6666
+ //
6667
+ // As faixas espelham os cortes diagnósticos de HbA1c da SBD (ver entrada
6668
+ // `HbA1c`) convertidos por essa fórmula, mantendo a eAG coerente com o
6669
+ // marcador de origem:
6670
+ // optimalMax 105 ← HbA1c 5,3 % (teto do controle ótimo)
6671
+ // max 117 ← HbA1c 5,7 % (teto do não diabético → início do pré-diabetes)
6672
+ // warningMax 137 ← HbA1c 6,4 % (teto do pré-diabetes → acima disso, diabetes)
6673
+ // optimalMin 82 ← HbA1c 4,5 % (piso do alvo fisiológico da entrada HbA1c)
6674
+ // min 11 ← HbA1c 2,0 % (mesmo piso de sanidade da entrada HbA1c)
6675
+ //
6676
+ // O piso era 70 nos dois campos, e não vinha desta conversão: 70 é o limite
6677
+ // inferior clássico da glicemia **de jejum**, transplantado para uma média
6678
+ // estimada de ~3 meses. Pela fórmula, 70 equivale a HbA1c 4,07 % — ou seja,
6679
+ // marcava como alterado exatamente quem a entrada `HbA1c` documenta que não
6680
+ // deve ser sinalizado, já que HbA1c baixa reflete hemólise, perda sanguínea
6681
+ // ou hemoglobinopatia, e não doença do metabolismo glicêmico. Espelhar o piso
6682
+ // do HbA1c mantém as duas entradas coerentes, que é o que o comentário sempre
6683
+ // afirmou fazer.
6684
+ //
6685
+ // Substitui a faixa anterior (optimalMax 126 = corte de glicemia de jejum;
6686
+ // max 154 = meta terapêutica do diabético, HbA1c 7 %), que classificava como
6687
+ // "Normal" valores já pré-diabéticos e diabéticos.
6688
+ eAG: {
6689
+ default: {
6690
+ max: 117,
6691
+ min: 11,
6692
+ minKind: "display",
6693
+ optimalMax: 105,
6694
+ optimalMin: 82,
6695
+ unit: "mg/dL",
6696
+ warningMax: 137
6697
+ },
6698
+ kind: "decision-threshold",
6699
+ source: "sbd-diabetes-2024"
6700
+ },
6701
+ // INR - International Normalized Ratio (non-anticoagulated patients)
6702
+ INR: {
6703
+ default: { max: 1.2, min: 0.8, optimalMax: 1.1, optimalMin: 0.9, unit: "ratio" },
6704
+ kind: "reference-interval",
6705
+ source: "tietz-7ed-2015"
6706
+ },
6707
+ // Prothrombin Time
6708
+ ProthrombinTime: {
6709
+ default: { max: 13.5, min: 11, optimalMax: 13, optimalMin: 11, unit: "seconds" },
6710
+ kind: "reference-interval",
6711
+ source: "tietz-7ed-2015"
6712
+ },
6713
+ // DHT - Dihydrotestosterone (adult male reference; female values are much lower)
6714
+ DHT: {
6715
+ default: { max: 85, min: 30, optimalMax: 85, optimalMin: 30, unit: "ng/dL" },
6716
+ kind: "reference-interval",
6717
+ source: "tietz-7ed-2015",
6741
6718
  variants: [
6742
6719
  {
6743
6720
  ageMin: 18,
6744
- range: { max: 1.5, min: 0, optimalMax: 1, optimalMin: 0, unit: "kg" },
6721
+ range: { max: 85, min: 30, optimalMax: 85, optimalMin: 30, unit: "ng/dL" },
6745
6722
  sex: "M"
6746
6723
  },
6747
6724
  {
6748
6725
  ageMin: 18,
6749
- range: { max: 1, min: 0, optimalMax: 0.5, optimalMin: 0, unit: "kg" },
6726
+ range: { max: 22, min: 4, optimalMax: 22, optimalMin: 4, unit: "ng/dL" },
6750
6727
  sex: "F"
6751
6728
  }
6752
6729
  ]
6753
6730
  },
6754
- VATVolume: {
6755
- default: {
6756
- max: 852,
6757
- min: 0,
6758
- optimalMax: 600,
6759
- optimalMin: 0,
6760
- unit: "cm\xB3",
6761
- warningMax: 1837
6762
- },
6731
+ // Total IgE - higher values suggest atopy/allergic sensitization
6732
+ IgE_Total: {
6733
+ default: { max: 100, min: 0, optimalMax: 100, optimalMin: 0, unit: "IU/mL" },
6763
6734
  direction: "lower-better",
6764
- source: "ge-corescan"
6765
- },
6766
- BMD_Total: {
6767
- default: { max: 1.4, min: 0.9, optimalMax: 1.3, optimalMin: 1, unit: "g/cm\xB2" },
6768
- direction: "higher-better",
6769
- source: "who-osteoporosis-1994"
6735
+ kind: "reference-interval",
6736
+ source: "tietz-7ed-2015"
6770
6737
  },
6771
- TScore_Total: {
6772
- default: { max: 4, min: -1, optimalMax: 2, optimalMin: -0.5, unit: "" },
6773
- direction: "higher-better",
6774
- source: "who-osteoporosis-1994"
6738
+ // IgG - Immunoglobulin G
6739
+ IgG: {
6740
+ default: { max: 1600, min: 700, optimalMax: 1600, optimalMin: 700, unit: "mg/dL" },
6741
+ kind: "reference-interval",
6742
+ source: "tietz-7ed-2015"
6775
6743
  },
6776
- ZScore_Total: {
6777
- default: { max: 2, min: -2, optimalMax: 1, optimalMin: -1, unit: "" },
6778
- direction: "higher-better",
6779
- source: "who-osteoporosis-1994"
6744
+ // IgE E1 Cat Dander - Class 0 (≤0.35 kU/L) = negative
6745
+ IgE_E1_CatDander: {
6746
+ default: { max: 0.35, min: 0, optimalMax: 0.35, optimalMin: 0, unit: "kU/L" },
6747
+ direction: "lower-better",
6748
+ kind: "decision-threshold",
6749
+ source: "tietz-7ed-2015"
6780
6750
  },
6781
- Amylase: {
6782
- default: { max: 100, min: 28, optimalMax: 90, optimalMin: 35, unit: "U/L" },
6751
+ // IgE GX1 Grasses - Class 0 (≤0.35 kU/L) = negative
6752
+ IgE_GX1_Grasses: {
6753
+ default: { max: 0.35, min: 0, optimalMax: 0.35, optimalMin: 0, unit: "kU/L" },
6754
+ direction: "lower-better",
6755
+ kind: "decision-threshold",
6783
6756
  source: "tietz-7ed-2015"
6757
+ }
6758
+ };
6759
+ var defaultReferenceRanges = Object.fromEntries(
6760
+ Object.entries(biomarkerRangeDefinitions).map(([code, def]) => [
6761
+ code,
6762
+ {
6763
+ ...def.default,
6764
+ ...def.kind && { kind: def.kind },
6765
+ ...def.source && { source: def.source }
6766
+ }
6767
+ ])
6768
+ );
6769
+ function getReferenceRange(testCode, context) {
6770
+ const definition = biomarkerRangeDefinitions[testCode];
6771
+ if (!definition) return void 0;
6772
+ const withSource = (range2, kind = definition.kind) => ({
6773
+ ...range2,
6774
+ ...kind && { kind },
6775
+ ...definition.source && { source: definition.source }
6776
+ });
6777
+ if (!context || !definition.variants || definition.variants.length === 0) {
6778
+ return withSource(definition.default);
6779
+ }
6780
+ const { age, biologicalSex, pregnancyTrimester, pregnant } = context;
6781
+ const hasPregnancyVariant = definition.variants.some((v) => v.pregnant === true);
6782
+ for (const variant of definition.variants) {
6783
+ if (variant.pregnant === true) {
6784
+ if (!pregnant) continue;
6785
+ if (variant.pregnancyTrimester !== void 0 && variant.pregnancyTrimester !== pregnancyTrimester) {
6786
+ continue;
6787
+ }
6788
+ } else if (pregnant && hasPregnancyVariant) {
6789
+ continue;
6790
+ }
6791
+ if (variant.sex !== "all" && variant.sex !== biologicalSex) {
6792
+ continue;
6793
+ }
6794
+ if (variant.ageMin !== void 0 && (age === void 0 || age < variant.ageMin)) {
6795
+ continue;
6796
+ }
6797
+ if (variant.ageMax !== void 0 && (age === void 0 || age > variant.ageMax)) {
6798
+ continue;
6799
+ }
6800
+ return withSource(variant.range, variant.kind ?? definition.kind);
6801
+ }
6802
+ return withSource(definition.default);
6803
+ }
6804
+ function getRangeDirection(testCode) {
6805
+ return biomarkerRangeDefinitions[testCode]?.direction ?? "range";
6806
+ }
6807
+ function referenceRangeMeaning(kind) {
6808
+ const system = "http://terminology.hl7.org/CodeSystem/referencerange-meaning";
6809
+ if (kind === "reference-interval") return { code: "normal", display: "Normal Range", system };
6810
+ if (kind === "decision-threshold") {
6811
+ return { code: "recommended", display: "Recommended Range", system };
6812
+ }
6813
+ return void 0;
6814
+ }
6815
+
6816
+ // src/converter.ts
6817
+ function interpretationCode(flag) {
6818
+ switch (flag) {
6819
+ case "H":
6820
+ return "H";
6821
+ // High
6822
+ case "L":
6823
+ return "L";
6824
+ // Low
6825
+ default:
6826
+ return "N";
6827
+ }
6828
+ }
6829
+ function interpretationDisplay(flag) {
6830
+ switch (flag) {
6831
+ case "H":
6832
+ return "High";
6833
+ case "L":
6834
+ return "Low";
6835
+ default:
6836
+ return "Normal";
6837
+ }
6838
+ }
6839
+ var SEX_CODING = {
6840
+ female: {
6841
+ code: "female",
6842
+ display: "Female",
6843
+ system: "http://hl7.org/fhir/administrative-gender"
6784
6844
  },
6785
- Creatinine_Urine: {
6786
- default: { max: 300, min: 20, optimalMax: 250, optimalMin: 40, unit: "mg/dL" },
6787
- source: "tietz-7ed-2015",
6788
- variants: [
6845
+ male: { code: "male", display: "Male", system: "http://hl7.org/fhir/administrative-gender" }
6846
+ };
6847
+ var buildReferenceRanges = (observation, quantity) => {
6848
+ const toRanges = (low, high, sex) => {
6849
+ if (low === void 0 && high === void 0) return [];
6850
+ return [
6789
6851
  {
6790
- ageMin: 18,
6791
- range: { max: 300, min: 40, optimalMax: 260, optimalMin: 60, unit: "mg/dL" },
6792
- sex: "M"
6793
- },
6852
+ ...sex === void 0 ? {} : { appliesTo: [{ coding: [SEX_CODING[sex]] }] },
6853
+ ...high === void 0 ? {} : { high: quantity(high) },
6854
+ ...low === void 0 ? {} : { low: quantity(low) }
6855
+ }
6856
+ ];
6857
+ };
6858
+ if (observation.referenceRanges && observation.referenceRanges.length > 0) {
6859
+ return observation.referenceRanges.flatMap((r) => toRanges(r.low, r.high, r.appliesTo));
6860
+ }
6861
+ const meaning = observation.referenceKind && referenceRangeMeaning(observation.referenceKind);
6862
+ return toRanges(observation.referenceMin, observation.referenceMax).map(
6863
+ (range2) => meaning ? { ...range2, type: { coding: [meaning] } } : range2
6864
+ );
6865
+ };
6866
+ function labObservationToFHIR(observation, patientId, laboratoryName) {
6867
+ const methodLoinc = observation.methodLoinc && methodVariantOf(observation.biomarkerCode, observation.methodLoinc) ? observation.methodLoinc : void 0;
6868
+ const loincCode = methodLoinc ?? codeToLoinc(observation.biomarkerCode);
6869
+ const sourceUnit = observation.unit || getDefaultUnit(observation.biomarkerCode) || observation.unit;
6870
+ const ucumUnit = resolveUcum(sourceUnit, observation.biomarkerCode);
6871
+ const quantity = (value) => ({
6872
+ ...ucumUnit ? { code: ucumUnit, system: "http://unitsofmeasure.org" } : {},
6873
+ ...sourceUnit ? { unit: sourceUnit } : {},
6874
+ value
6875
+ });
6876
+ const isQualitative = observation.isQualitative || typeof observation.value === "string";
6877
+ const fhirObs = {
6878
+ category: [
6879
+ {
6880
+ coding: [
6881
+ {
6882
+ code: "laboratory",
6883
+ display: "Laboratory",
6884
+ system: "http://terminology.hl7.org/CodeSystem/observation-category"
6885
+ }
6886
+ ]
6887
+ }
6888
+ ],
6889
+ code: {
6890
+ // Sem LOINC, o coding LOINC simplesmente não sai. Antes ia `99999-9`,
6891
+ // que não é código LOINC nenhum: publicava sob `http://loinc.org` uma
6892
+ // afirmação falsa, e quem consumisse o bundle confiando no system
6893
+ // trataria aquilo como código de verdade. Composição corporal, densidade
6894
+ // óssea e escore de cálcio não têm LOINC, e o certo é a lacuna explícita.
6895
+ coding: [
6896
+ ...loincCode ? [
6897
+ {
6898
+ code: loincCode,
6899
+ display: observation.biomarkerName,
6900
+ system: LOINC_SYSTEM
6901
+ }
6902
+ ] : [],
6903
+ {
6904
+ code: observation.biomarkerCode,
6905
+ display: observation.biomarkerName,
6906
+ system: BIOMARKER_CODE_SYSTEM
6907
+ }
6908
+ ],
6909
+ text: observation.biomarkerName
6910
+ },
6911
+ effectiveDateTime: observation.collectionDate,
6912
+ id: `${observation.reportId}-${observation.biomarkerCode}`,
6913
+ interpretation: [
6914
+ {
6915
+ coding: [
6916
+ {
6917
+ code: interpretationCode(observation.flag),
6918
+ display: interpretationDisplay(observation.flag),
6919
+ system: "http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation"
6920
+ }
6921
+ ]
6922
+ }
6923
+ ],
6924
+ performer: laboratoryName ? [{ display: laboratoryName }] : void 0,
6925
+ resourceType: "Observation",
6926
+ status: "final",
6927
+ subject: {
6928
+ reference: `Patient/${patientId}`
6929
+ }
6930
+ };
6931
+ if (isQualitative) {
6932
+ fhirObs.valueString = String(observation.value);
6933
+ } else {
6934
+ fhirObs.valueQuantity = quantity(observation.value);
6935
+ const referenceRange = buildReferenceRanges(observation, quantity);
6936
+ if (referenceRange.length > 0) fhirObs.referenceRange = referenceRange;
6937
+ }
6938
+ return fhirObs;
6939
+ }
6940
+ function labReportToFHIR(report, patientId, observationIds) {
6941
+ let status;
6942
+ switch (report.processingStatus) {
6943
+ case "complete":
6944
+ status = "final";
6945
+ break;
6946
+ case "partial":
6947
+ status = "partial";
6948
+ break;
6949
+ case "pending_review":
6950
+ status = "preliminary";
6951
+ break;
6952
+ default:
6953
+ status = "final";
6954
+ }
6955
+ return {
6956
+ category: [
6957
+ {
6958
+ coding: [
6959
+ {
6960
+ code: "LAB",
6961
+ display: "Laboratory",
6962
+ system: "http://terminology.hl7.org/CodeSystem/v2-0074"
6963
+ }
6964
+ ]
6965
+ }
6966
+ ],
6967
+ code: {
6968
+ coding: [
6969
+ {
6970
+ code: "11502-2",
6971
+ // Laboratory report
6972
+ display: "Laboratory report",
6973
+ system: "http://loinc.org"
6974
+ }
6975
+ ],
6976
+ text: "Laboratory Results"
6977
+ },
6978
+ conclusion: report.overallStatus === "NORMAL" ? "All results within normal limits" : "One or more abnormal results detected",
6979
+ conclusionCode: report.overallStatus === "ANORMAL" ? [
6794
6980
  {
6795
- ageMin: 18,
6796
- range: { max: 220, min: 20, optimalMax: 180, optimalMin: 40, unit: "mg/dL" },
6797
- sex: "F"
6981
+ coding: [
6982
+ {
6983
+ code: "A",
6984
+ display: "Abnormal",
6985
+ system: "http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation"
6986
+ }
6987
+ ]
6798
6988
  }
6799
- ]
6800
- },
6801
- IgA: {
6802
- default: { max: 400, min: 70, optimalMax: 350, optimalMin: 100, unit: "mg/dL" },
6803
- source: "tietz-7ed-2015"
6804
- },
6805
- Lipase: {
6806
- default: { max: 60, min: 0, optimalMax: 50, optimalMin: 10, unit: "U/L" },
6807
- source: "tietz-7ed-2015"
6808
- },
6809
- Microalbumin_Urine: {
6810
- default: { max: 30, min: 0, optimalMax: 20, optimalMin: 0, unit: "mg/L" },
6811
- source: "kdigo-ckd-2024"
6812
- },
6813
- PSA_Free: {
6814
- default: { max: 1.5, min: 0, optimalMax: 1, optimalMin: 0, unit: "ng/mL" },
6815
- source: "sturgeon-nacb-2008"
6816
- },
6817
- // Relação PSA livre/total — `higher-better`: razão maior = menor risco de
6818
- // câncer de próstata, logo não há limite superior "ruim" (acima do normal é
6819
- // verde). Faixas alinhadas à tabela clínica: ≥25% baixo risco (verde),
6820
- // 10–25% atenção — intermediário/moderado (âmbar), <10% alto risco >50%
6821
- // (vermelho). Limites: `min` (25) é o piso do verde e `warningMin` (10) o
6822
- // piso do âmbar — o vermelho é estritamente abaixo de 10% (< 10%); 10%
6823
- // pertence ao âmbar, conforme a faixa "10–15% moderado" da tabela.
6824
- PSA_FreeRatio: {
6825
- default: { max: 100, min: 25, optimalMax: 100, optimalMin: 30, unit: "%", warningMin: 10 },
6826
- direction: "higher-better",
6827
- source: "sturgeon-nacb-2008"
6828
- },
6829
- // BMI - WHO classification: 18.5–24.9 normal, 25–29.9 overweight, ≥30 obese
6830
- BMI: {
6831
- default: { max: 30, min: 18.5, optimalMax: 24.9, optimalMin: 18.5, unit: "kg/m2" },
6832
- source: "who-obesity-2000"
6833
- },
6834
- // eAG — Estimated Average Glucose, derivada da HbA1c pela fórmula ADAG:
6835
- // eAG (mg/dL) = 28,7 × HbA1c(%) − 46,7 (NATHAN, D. M. et al. Translating the
6836
- // A1C assay into estimated average glucose values. Diabetes Care, v. 31, n. 8,
6837
- // p. 1473-1478, 2008. DOI: 10.2337/dc08-0545).
6838
- //
6839
- // As faixas espelham os cortes diagnósticos de HbA1c da SBD (ver entrada
6840
- // `HbA1c`) convertidos por essa fórmula, mantendo a eAG coerente com o
6841
- // marcador de origem:
6842
- // optimalMax 105 ← HbA1c 5,3 % (teto do controle ótimo)
6843
- // max 117 ← HbA1c 5,7 % (teto do não diabético → início do pré-diabetes)
6844
- // warningMax 137 ← HbA1c 6,4 % (teto do pré-diabetes → acima disso, diabetes)
6845
- // optimalMin 82 ← HbA1c 4,5 % (piso do alvo fisiológico da entrada HbA1c)
6846
- // min 11 ← HbA1c 2,0 % (mesmo piso de sanidade da entrada HbA1c)
6847
- //
6848
- // O piso era 70 nos dois campos, e não vinha desta conversão: 70 é o limite
6849
- // inferior clássico da glicemia **de jejum**, transplantado para uma média
6850
- // estimada de ~3 meses. Pela fórmula, 70 equivale a HbA1c 4,07 % — ou seja,
6851
- // marcava como alterado exatamente quem a entrada `HbA1c` documenta que não
6852
- // deve ser sinalizado, já que HbA1c baixa reflete hemólise, perda sanguínea
6853
- // ou hemoglobinopatia, e não doença do metabolismo glicêmico. Espelhar o piso
6854
- // do HbA1c mantém as duas entradas coerentes, que é o que o comentário sempre
6855
- // afirmou fazer.
6856
- //
6857
- // Substitui a faixa anterior (optimalMax 126 = corte de glicemia de jejum;
6858
- // max 154 = meta terapêutica do diabético, HbA1c 7 %), que classificava como
6859
- // "Normal" valores já pré-diabéticos e diabéticos.
6860
- eAG: {
6861
- default: { max: 117, min: 11, optimalMax: 105, optimalMin: 82, unit: "mg/dL", warningMax: 137 },
6862
- source: "sbd-diabetes-2024"
6863
- },
6864
- // INR - International Normalized Ratio (non-anticoagulated patients)
6865
- INR: {
6866
- default: { max: 1.2, min: 0.8, optimalMax: 1.1, optimalMin: 0.9, unit: "ratio" },
6867
- source: "tietz-7ed-2015"
6868
- },
6869
- // Prothrombin Time
6870
- ProthrombinTime: {
6871
- default: { max: 13.5, min: 11, optimalMax: 13, optimalMin: 11, unit: "seconds" },
6872
- source: "tietz-7ed-2015"
6873
- },
6874
- // DHT - Dihydrotestosterone (adult male reference; female values are much lower)
6875
- DHT: {
6876
- default: { max: 85, min: 30, optimalMax: 85, optimalMin: 30, unit: "ng/dL" },
6877
- source: "tietz-7ed-2015",
6878
- variants: [
6989
+ ] : void 0,
6990
+ effectiveDateTime: report.collectionDate,
6991
+ id: report.reportId,
6992
+ issued: report.createdAt,
6993
+ performer: report.laboratoryName ? [{ display: report.laboratoryName }] : void 0,
6994
+ resourceType: "DiagnosticReport",
6995
+ result: observationIds.map((id) => ({ reference: `Observation/${id}` })),
6996
+ status,
6997
+ subject: {
6998
+ reference: `Patient/${patientId}`
6999
+ }
7000
+ };
7001
+ }
7002
+ function userProfileToFHIR(profile) {
7003
+ const nameParts = profile.name.split(" ");
7004
+ const given = nameParts.slice(0, -1);
7005
+ const family = nameParts[nameParts.length - 1] || "";
7006
+ return {
7007
+ address: profile.address ? [
6879
7008
  {
6880
- ageMin: 18,
6881
- range: { max: 85, min: 30, optimalMax: 85, optimalMin: 30, unit: "ng/dL" },
6882
- sex: "M"
6883
- },
7009
+ city: profile.address.city,
7010
+ country: profile.address.country || "BR",
7011
+ line: [
7012
+ profile.address.street && profile.address.number ? `${profile.address.street}, ${profile.address.number}` : profile.address.street,
7013
+ profile.address.complement
7014
+ ].filter(Boolean),
7015
+ postalCode: profile.address.postalCode,
7016
+ state: profile.address.state
7017
+ }
7018
+ ] : void 0,
7019
+ birthDate: profile.birthDate,
7020
+ gender: profile.gender,
7021
+ id: profile.userId,
7022
+ name: [
6884
7023
  {
6885
- ageMin: 18,
6886
- range: { max: 22, min: 4, optimalMax: 22, optimalMin: 4, unit: "ng/dL" },
6887
- sex: "F"
7024
+ family,
7025
+ given: given.length > 0 ? given : void 0,
7026
+ text: profile.name
6888
7027
  }
6889
- ]
6890
- },
6891
- // Total IgE - higher values suggest atopy/allergic sensitization
6892
- IgE_Total: {
6893
- default: { max: 100, min: 0, optimalMax: 100, optimalMin: 0, unit: "IU/mL" },
6894
- direction: "lower-better",
6895
- source: "tietz-7ed-2015"
6896
- },
6897
- // IgG - Immunoglobulin G
6898
- IgG: {
6899
- default: { max: 1600, min: 700, optimalMax: 1600, optimalMin: 700, unit: "mg/dL" },
6900
- source: "tietz-7ed-2015"
6901
- },
6902
- // IgE E1 Cat Dander - Class 0 (≤0.35 kU/L) = negative
6903
- IgE_E1_CatDander: {
6904
- default: { max: 0.35, min: 0, optimalMax: 0.35, optimalMin: 0, unit: "kU/L" },
6905
- direction: "lower-better",
6906
- source: "tietz-7ed-2015"
6907
- },
6908
- // IgE GX1 Grasses - Class 0 (≤0.35 kU/L) = negative
6909
- IgE_GX1_Grasses: {
6910
- default: { max: 0.35, min: 0, optimalMax: 0.35, optimalMin: 0, unit: "kU/L" },
6911
- direction: "lower-better",
6912
- source: "tietz-7ed-2015"
7028
+ ],
7029
+ resourceType: "Patient",
7030
+ telecom: [
7031
+ ...profile.email ? [{ system: "email", value: profile.email }] : [],
7032
+ ...profile.phone ? [{ system: "phone", value: profile.phone }] : []
7033
+ ].length > 0 ? [
7034
+ ...profile.email ? [{ system: "email", value: profile.email }] : [],
7035
+ ...profile.phone ? [{ system: "phone", value: profile.phone }] : []
7036
+ ] : void 0
7037
+ };
7038
+ }
7039
+ function labResultToFHIRBundle(report, observations, userProfile) {
7040
+ const patientId = userProfile.userId;
7041
+ const fhirObservations = observations.map((obs) => {
7042
+ const resource = labObservationToFHIR(
7043
+ { ...obs, collectionDate: report.collectionDate },
7044
+ patientId,
7045
+ report.laboratoryName
7046
+ );
7047
+ return { fullUrl: entryFullUrl(resource), resource };
7048
+ });
7049
+ const observationIds = fhirObservations.map((entry) => entry.resource.id);
7050
+ const diagnosticReport = labReportToFHIR(report, patientId, observationIds);
7051
+ const fhirPatient = userProfileToFHIR(userProfile);
7052
+ return {
7053
+ entry: [
7054
+ { fullUrl: entryFullUrl(fhirPatient), resource: fhirPatient },
7055
+ { fullUrl: entryFullUrl(diagnosticReport), resource: diagnosticReport },
7056
+ ...fhirObservations
7057
+ ],
7058
+ resourceType: "Bundle",
7059
+ type: "collection"
7060
+ };
7061
+ }
7062
+
7063
+ // src/cli/commands/convert.ts
7064
+ async function convert(args, json) {
7065
+ const raw = await getInput(args[0]);
7066
+ const data = parseJson(
7067
+ raw,
7068
+ "JSON inv\xE1lido. Forne\xE7a um objeto com { report, observations, profile }."
7069
+ );
7070
+ if (!data.report || !data.observations || !data.profile) {
7071
+ exitWithError(
7072
+ 'Formato esperado: { "report": LabReportData, "observations": LabObservationData[], "profile": UserProfileData }'
7073
+ );
6913
7074
  }
6914
- };
6915
- var defaultReferenceRanges = Object.fromEntries(
6916
- Object.entries(biomarkerRangeDefinitions).map(([code, def]) => [
6917
- code,
6918
- def.source ? { ...def.default, source: def.source } : def.default
6919
- ])
6920
- );
6921
- function getReferenceRange(testCode, context) {
6922
- const definition = biomarkerRangeDefinitions[testCode];
6923
- if (!definition) return void 0;
6924
- const withSource = (range2) => definition.source ? { ...range2, source: definition.source } : range2;
6925
- if (!context || !definition.variants || definition.variants.length === 0) {
6926
- return withSource(definition.default);
7075
+ const bundle = labResultToFHIRBundle(data.report, data.observations, data.profile);
7076
+ if (json) {
7077
+ outputJson(bundle);
7078
+ } else {
7079
+ outputText(JSON.stringify(bundle, null, 2));
7080
+ }
7081
+ }
7082
+
7083
+ // src/validators.ts
7084
+ function validateFHIRImportBundle(data) {
7085
+ const errors = [];
7086
+ if (!data || typeof data !== "object") {
7087
+ errors.push({ details: "Input must be a JSON object", field: "root" });
7088
+ return errors;
7089
+ }
7090
+ const bundle = data;
7091
+ if (bundle.resourceType !== "Bundle") {
7092
+ errors.push({
7093
+ details: `Expected resourceType "Bundle", got "${String(bundle.resourceType)}"`,
7094
+ field: "resourceType"
7095
+ });
7096
+ }
7097
+ if (!Array.isArray(bundle.entry)) {
7098
+ errors.push({ details: 'Bundle must contain an "entry" array', field: "entry" });
7099
+ } else if (bundle.entry.length === 0) {
7100
+ errors.push({ details: "Bundle entry array is empty", field: "entry" });
7101
+ }
7102
+ return errors;
7103
+ }
7104
+
7105
+ // src/importer.ts
7106
+ var MAX_OBSERVATIONS = 5e3;
7107
+ var MAX_FILE_SIZE = 15 * 1024 * 1024;
7108
+ function resolveBiomarkerCode(observation) {
7109
+ const coding = observation.code?.coding ?? [];
7110
+ const loincCode = coding.find((c) => c.system === LOINC_SYSTEM)?.code;
7111
+ const declaredCode = coding.find((c) => c.system === BIOMARKER_CODE_SYSTEM)?.code;
7112
+ const seenCodes = [
7113
+ ...loincCode ? [`LOINC ${loincCode}`] : [],
7114
+ ...declaredCode ? [`biomarker code ${declaredCode}`] : []
7115
+ ];
7116
+ let reason;
7117
+ if (seenCodes.length > 0) {
7118
+ reason = `Unknown code: ${seenCodes.join(", ")}`;
7119
+ } else if (coding.length > 0) {
7120
+ const systems = [...new Set(coding.map((c) => c.system ?? "(sem system)"))];
7121
+ reason = `No code in a supported system (found: ${systems.join(", ")})`;
7122
+ } else {
7123
+ reason = "No code found in observation coding";
6927
7124
  }
6928
- const { age, biologicalSex, pregnancyTrimester, pregnant } = context;
6929
- const hasPregnancyVariant = definition.variants.some((v) => v.pregnant === true);
6930
- for (const variant of definition.variants) {
6931
- if (variant.pregnant === true) {
6932
- if (!pregnant) continue;
6933
- if (variant.pregnancyTrimester !== void 0 && variant.pregnancyTrimester !== pregnancyTrimester) {
6934
- continue;
6935
- }
6936
- } else if (pregnant && hasPregnancyVariant) {
7125
+ const fromLoinc = loincCode ? loincToCode(loincCode) : void 0;
7126
+ if (fromLoinc) return { internalCode: fromLoinc, loincCode, reason };
7127
+ if (declaredCode && isValidCode(declaredCode)) {
7128
+ const canonical = normalizeCode(declaredCode);
7129
+ return { internalCode: canonical, loincCode: codeToLoinc(canonical), reason };
7130
+ }
7131
+ return { loincCode, reason };
7132
+ }
7133
+ function extractFlag(observation) {
7134
+ const code = observation.interpretation?.[0]?.coding?.[0]?.code;
7135
+ if (code === "H" || code === "HH") return "H";
7136
+ if (code === "L" || code === "LL") return "L";
7137
+ return "";
7138
+ }
7139
+ function extractObservationsFromBundle(bundle) {
7140
+ const observations = [];
7141
+ const skipped = [];
7142
+ for (let i = 0; i < bundle.entry.length; i++) {
7143
+ const entry = bundle.entry[i];
7144
+ if (!entry.resource) {
7145
+ skipped.push({ index: i, reason: "Entry has no resource" });
6937
7146
  continue;
6938
7147
  }
6939
- if (variant.sex !== "all" && variant.sex !== biologicalSex) {
7148
+ if (entry.resource.resourceType !== "Observation") {
6940
7149
  continue;
6941
7150
  }
6942
- if (variant.ageMin !== void 0 && (age === void 0 || age < variant.ageMin)) {
7151
+ if (observations.length >= MAX_OBSERVATIONS) {
7152
+ skipped.push({ index: i, reason: `Maximum of ${MAX_OBSERVATIONS} observations exceeded` });
6943
7153
  continue;
6944
7154
  }
6945
- if (variant.ageMax !== void 0 && (age === void 0 || age > variant.ageMax)) {
6946
- continue;
7155
+ observations.push(entry.resource);
7156
+ }
7157
+ return { observations, skipped };
7158
+ }
7159
+ function mapFHIRObservationToInternal(observation, index) {
7160
+ const { internalCode, loincCode, reason } = resolveBiomarkerCode(observation);
7161
+ if (!internalCode) {
7162
+ return {
7163
+ skipped: { index, loincCode, reason, resourceType: "Observation" }
7164
+ };
7165
+ }
7166
+ const definition = getDefinitionByCode(internalCode);
7167
+ let value;
7168
+ let unit = "";
7169
+ let isQualitative = false;
7170
+ if (observation.valueQuantity?.value !== void 0) {
7171
+ value = observation.valueQuantity.value;
7172
+ unit = observation.valueQuantity.unit || observation.valueQuantity.code || "";
7173
+ } else if (observation.valueString) {
7174
+ value = observation.valueString;
7175
+ isQualitative = true;
7176
+ } else {
7177
+ return {
7178
+ skipped: {
7179
+ index,
7180
+ loincCode,
7181
+ reason: "Observation has no value (valueQuantity or valueString)",
7182
+ resourceType: "Observation"
7183
+ }
7184
+ };
7185
+ }
7186
+ const collectionDate = observation.effectiveDateTime || observation.effectivePeriod?.start || "";
7187
+ if (!collectionDate) {
7188
+ return {
7189
+ skipped: {
7190
+ index,
7191
+ loincCode,
7192
+ reason: "Observation has no effectiveDateTime or effectivePeriod.start",
7193
+ resourceType: "Observation"
7194
+ }
7195
+ };
7196
+ }
7197
+ let referenceMin;
7198
+ let referenceMax;
7199
+ let referenceKind;
7200
+ if (observation.referenceRange?.[0]) {
7201
+ referenceMin = observation.referenceRange[0].low?.value;
7202
+ referenceMax = observation.referenceRange[0].high?.value;
7203
+ const meaning = observation.referenceRange[0].type?.coding?.find(
7204
+ (c) => c.system === "http://terminology.hl7.org/CodeSystem/referencerange-meaning"
7205
+ )?.code;
7206
+ if (meaning === "normal") referenceKind = "reference-interval";
7207
+ else if (meaning === "recommended") referenceKind = "decision-threshold";
7208
+ }
7209
+ const imported = {
7210
+ biomarkerCode: internalCode,
7211
+ biomarkerName: definition?.names.pt[0] || definition?.names.en[0] || observation.code.text || internalCode,
7212
+ collectionDate,
7213
+ flag: extractFlag(observation),
7214
+ isQualitative,
7215
+ loincCode,
7216
+ ...loincCode && methodVariantOf(internalCode, loincCode) && { methodLoinc: loincCode },
7217
+ ...referenceKind && { referenceKind },
7218
+ referenceMax,
7219
+ referenceMin,
7220
+ unit: unit || definition?.unit || "",
7221
+ value
7222
+ };
7223
+ return { observation: imported };
7224
+ }
7225
+ function processImportBundle(data) {
7226
+ const validationErrors = validateFHIRImportBundle(data);
7227
+ if (validationErrors.length > 0) {
7228
+ return {
7229
+ errors: validationErrors,
7230
+ imported: [],
7231
+ skipped: [],
7232
+ totalProcessed: 0
7233
+ };
7234
+ }
7235
+ const bundle = data;
7236
+ const { observations, skipped } = extractObservationsFromBundle(bundle);
7237
+ const imported = [];
7238
+ const allSkipped = [...skipped];
7239
+ for (let i = 0; i < observations.length; i++) {
7240
+ const result = mapFHIRObservationToInternal(observations[i], i);
7241
+ if ("observation" in result) {
7242
+ imported.push(result.observation);
7243
+ } else {
7244
+ allSkipped.push(result.skipped);
6947
7245
  }
6948
- return withSource(variant.range);
6949
7246
  }
6950
- return withSource(definition.default);
7247
+ return {
7248
+ errors: [],
7249
+ imported,
7250
+ skipped: allSkipped,
7251
+ totalProcessed: observations.length
7252
+ };
6951
7253
  }
6952
- function getRangeDirection(testCode) {
6953
- return biomarkerRangeDefinitions[testCode]?.direction ?? "range";
7254
+
7255
+ // src/cli/commands/import.ts
7256
+ async function importBundle(args, json) {
7257
+ const raw = await getInput(args[0]);
7258
+ const bundle = parseJson(raw, "JSON inv\xE1lido.");
7259
+ const result = processImportBundle(bundle);
7260
+ if (json) {
7261
+ outputJson(result);
7262
+ return;
7263
+ }
7264
+ outputText(`Processados: ${result.totalProcessed}`);
7265
+ outputText(`Importados: ${result.imported.length}`);
7266
+ outputText(`Ignorados: ${result.skipped.length}`);
7267
+ outputText(`Erros: ${result.errors.length}`);
7268
+ if (result.imported.length > 0) {
7269
+ outputText("\nObserva\xE7\xF5es importadas:");
7270
+ for (const obs of result.imported) {
7271
+ outputText(` ${obs.biomarkerCode} (${obs.loincCode}): ${obs.value} ${obs.unit}`);
7272
+ }
7273
+ }
7274
+ if (result.errors.length > 0) {
7275
+ outputText("\nErros:");
7276
+ for (const err of result.errors) {
7277
+ outputText(` ${err.field}: ${err.details}`);
7278
+ }
7279
+ }
7280
+ }
7281
+
7282
+ // src/cli/commands/list.ts
7283
+ import { parseArgs } from "util";
7284
+ async function list(args, json) {
7285
+ const { values } = parseArgs({
7286
+ args,
7287
+ options: {
7288
+ category: { type: "string" },
7289
+ visible: { default: false, type: "boolean" }
7290
+ },
7291
+ strict: false
7292
+ });
7293
+ let defs;
7294
+ if (values.category) {
7295
+ defs = getBiomarkersByCategory(values.category);
7296
+ if (defs.length === 0) {
7297
+ exitWithError(`Categoria n\xE3o encontrada ou vazia: ${values.category}`);
7298
+ }
7299
+ } else if (values.visible) {
7300
+ defs = getVisibleDefinitions();
7301
+ } else {
7302
+ defs = getAllDefinitions();
7303
+ }
7304
+ if (json) {
7305
+ outputJson(defs);
7306
+ return;
7307
+ }
7308
+ const rows = defs.map((d) => [
7309
+ d.code,
7310
+ d.loinc ?? "\u2014",
7311
+ d.names.pt[0] ?? "",
7312
+ d.unit ?? "\u2014",
7313
+ Array.isArray(d.category) ? d.category.join(", ") : d.category
7314
+ ]);
7315
+ outputText(formatTable(["C\xF3digo", "LOINC", "Nome (pt)", "Unidade", "Categoria"], rows));
7316
+ outputText(`
7317
+ Total: ${defs.length} biomarcadores`);
7318
+ }
7319
+
7320
+ // src/cli/commands/loinc-map.ts
7321
+ async function loincMap(_args, json) {
7322
+ const defs = getAllDefinitions().filter((d) => d.loinc);
7323
+ if (json) {
7324
+ const map = Object.fromEntries(defs.map((d) => [d.loinc, d.code]));
7325
+ outputJson(map);
7326
+ return;
7327
+ }
7328
+ const rows = defs.map((d) => [d.loinc, d.code, d.names.pt[0] ?? d.names.en[0] ?? ""]);
7329
+ outputText(formatTable(["LOINC", "C\xF3digo", "Nome (pt)"], rows));
7330
+ outputText(`
7331
+ Total: ${rows.length} mapeamentos`);
7332
+ }
7333
+
7334
+ // src/cli/commands/lookup.ts
7335
+ function displayDefinition(def) {
7336
+ const cat = Array.isArray(def.category) ? def.category.join(", ") : def.category;
7337
+ outputText(
7338
+ [
7339
+ `Biomarcador: ${def.code}`,
7340
+ ` LOINC: ${def.loinc ?? "\u2014"}`,
7341
+ ` Nomes (pt): ${def.names.pt.join(", ")}`,
7342
+ ` Nomes (en): ${def.names.en.join(", ")}`,
7343
+ ` Categoria: ${cat}`,
7344
+ ` Unidade: ${def.unit ?? "\u2014"}`,
7345
+ ` Sexo: ${def.sex ?? "ambos"}`,
7346
+ ` Vis\xEDvel: ${def.hidden ? "n\xE3o" : "sim"}`
7347
+ ].join("\n")
7348
+ );
7349
+ }
7350
+ async function lookup(args, json) {
7351
+ const code = args[0];
7352
+ if (!code) exitWithError("Uso: fhir-bio lookup <c\xF3digo>");
7353
+ const def = getDefinitionByCode(code);
7354
+ if (!def) exitWithError(`Biomarcador n\xE3o encontrado: ${code}`);
7355
+ if (json) {
7356
+ outputJson(def);
7357
+ } else {
7358
+ displayDefinition(def);
7359
+ }
7360
+ }
7361
+ async function lookupLoinc(args, json) {
7362
+ const loinc = args[0];
7363
+ if (!loinc) exitWithError("Uso: fhir-bio lookup-loinc <loinc>");
7364
+ const def = getDefinitionByLoinc(loinc);
7365
+ if (!def) exitWithError(`Biomarcador n\xE3o encontrado para LOINC: ${loinc}`);
7366
+ if (json) {
7367
+ outputJson(def);
7368
+ } else {
7369
+ displayDefinition(def);
7370
+ }
6954
7371
  }
6955
7372
 
6956
7373
  // src/cli/commands/range.ts
7374
+ import { parseArgs as parseArgs2 } from "util";
6957
7375
  async function range(args, json) {
6958
7376
  const { positionals, values } = parseArgs2({
6959
7377
  allowPositionals: true,
@@ -6990,11 +7408,18 @@ async function range(args, json) {
6990
7408
  const sexLabel = ctx.biologicalSex === "M" ? "Homem" : ctx.biologicalSex === "F" ? "Mulher" : "Geral";
6991
7409
  const ageLabel = ctx.age !== void 0 ? `, ${ctx.age} anos` : "";
6992
7410
  const fmt = (v) => v !== void 0 ? String(v) : "\u2014";
7411
+ const desenho = (kind) => kind === "display" ? " (desenho, n\xE3o \xE9 corte)" : "";
7412
+ const tipos = {
7413
+ "decision-threshold": "limiar de decis\xE3o",
7414
+ population: "distribui\xE7\xE3o populacional",
7415
+ "reference-interval": "intervalo de refer\xEAncia"
7416
+ };
6993
7417
  outputText(
6994
7418
  [
6995
7419
  `Faixa de Refer\xEAncia: ${code} (${sexLabel}${ageLabel})`,
6996
- ` M\xEDnimo: ${fmt(ref.min)} ${ref.unit}`,
6997
- ` M\xE1ximo: ${fmt(ref.max)} ${ref.unit}`,
7420
+ ` Tipo: ${ref.kind ? tipos[ref.kind] : "\u2014"}`,
7421
+ ` M\xEDnimo: ${fmt(ref.min)} ${ref.unit}${desenho(ref.minKind)}`,
7422
+ ` M\xE1ximo: ${fmt(ref.max)} ${ref.unit}${desenho(ref.maxKind)}`,
6998
7423
  ` \xD3timo (m\xEDn): ${fmt(ref.optimalMin)} ${ref.unit}`,
6999
7424
  ` \xD3timo (m\xE1x): ${fmt(ref.optimalMax)} ${ref.unit}`,
7000
7425
  ` Alerta (m\xE1x): ${fmt(ref.warningMax)} ${ref.unit}`,
@@ -7487,7 +7912,7 @@ var COMMANDS = {
7487
7912
  async function main() {
7488
7913
  const { command, help, json, resto, version } = dividirArgv(process.argv.slice(2));
7489
7914
  if (version) {
7490
- process.stdout.write(`${"0.33.0"}
7915
+ process.stdout.write(`${"0.35.0"}
7491
7916
  `);
7492
7917
  return;
7493
7918
  }