med-pdf-nmo 0.1.1 → 0.1.2

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.
@@ -32401,6 +32401,8 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
32401
32401
  singleTieSpecificityGap: 0.5,
32402
32402
  sharedMultiSegmentBoost: true,
32403
32403
  countRelationBoost: true,
32404
+ optionFamilyComparatorGuard: true,
32405
+ optionFamilyCompactComboGuard: true,
32404
32406
  topQuestionChunks: 28,
32405
32407
  evidenceLimit: 8
32406
32408
  };
@@ -32581,6 +32583,100 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
32581
32583
  keyedRuntimeCache.clear();
32582
32584
  }
32583
32585
 
32586
+ // src/predictor/scorer-registry.ts
32587
+ var SELECTION_STRUCTURAL_EVIDENCE_WEIGHTS_RECORD = {
32588
+ coordinate_table_row: 1.25,
32589
+ coordinate_table_group: 1.25,
32590
+ coordinate_table_group_inverse: 1.25,
32591
+ coordinate_table_multicell_row: 1.25,
32592
+ coordinate_table_membership: 1.15,
32593
+ parenthetical_group_segment: 1.05,
32594
+ preceding_question_label: 1.05,
32595
+ question_continuation_list: 1.05,
32596
+ exact_numeric_option_segment: 1.05,
32597
+ exact_hour_alias_segment: 1.05,
32598
+ short_medical_alias_segment: 0.9,
32599
+ visual_table_column: 1.2,
32600
+ exact_short_label_visual_row: 1.15,
32601
+ short_label_visual_row: 1.05,
32602
+ answer_ordinal_row: 1.05,
32603
+ fibrosis_stage_row: 1.2,
32604
+ gene_sentence_segment: 1.1,
32605
+ clinical_feature_segment: 1,
32606
+ mkb_class_exclusion_absent: 1,
32607
+ classification_code_segment: 1.15,
32608
+ label_number_proximity: 1,
32609
+ label_definition_segment: 1,
32610
+ definition_exact_answer_segment: 1,
32611
+ definition_completion_specificity: 0.95,
32612
+ row_label_segment: 0.95,
32613
+ bounded_list_segment: 0.95,
32614
+ ordinal_list_segment: 0.9,
32615
+ drug_dose_segment: 0.9,
32616
+ frequency_polarity_segment: 0.9,
32617
+ frequency_polarity_list_item: 0.95,
32618
+ clinical_course_cue_segment: 0.9,
32619
+ recommendation_block_segment: 0.85,
32620
+ recommendation_item_segment: 0.85,
32621
+ explicit_recommendation_target_segment: 0.85,
32622
+ numeric_condition_less_than: 0.85,
32623
+ numeric_condition_more_than: 0.85,
32624
+ numeric_condition_equal: 0.85,
32625
+ conditioned_number_segment: 0.8,
32626
+ cloze_gap_local: 0.8
32627
+ };
32628
+ var SELECTION_STRUCTURAL_EVIDENCE_WEIGHTS = new Map(Object.entries(SELECTION_STRUCTURAL_EVIDENCE_WEIGHTS_RECORD));
32629
+ var CONFIDENCE_STRUCTURAL_EVIDENCE_KINDS = /* @__PURE__ */ new Set([
32630
+ "coordinate_table_row",
32631
+ "coordinate_table_group",
32632
+ "coordinate_table_group_inverse",
32633
+ "coordinate_table_multicell_row",
32634
+ "coordinate_table_membership",
32635
+ "parenthetical_group_segment",
32636
+ "preceding_question_label",
32637
+ "question_continuation_list",
32638
+ "exact_numeric_option_segment",
32639
+ "exact_hour_alias_segment",
32640
+ "visual_table_column",
32641
+ "exact_short_label_visual_row",
32642
+ "short_label_visual_row",
32643
+ "answer_ordinal_row",
32644
+ "fibrosis_stage_row",
32645
+ "gene_sentence_segment",
32646
+ "clinical_feature_segment",
32647
+ "mkb_class_exclusion_absent",
32648
+ "classification_code_segment",
32649
+ "label_number_proximity",
32650
+ "label_definition_segment",
32651
+ "definition_exact_answer_segment",
32652
+ "definition_completion_specificity",
32653
+ "row_label_segment",
32654
+ "bounded_list_segment",
32655
+ "ordinal_list_segment",
32656
+ "drug_dose_segment",
32657
+ "frequency_polarity_segment",
32658
+ "frequency_polarity_list_item",
32659
+ "clinical_course_cue_segment",
32660
+ "recommendation_block_segment",
32661
+ "recommendation_item_segment",
32662
+ "explicit_recommendation_target_segment",
32663
+ "numeric_condition_less_than",
32664
+ "numeric_condition_more_than",
32665
+ "numeric_condition_equal",
32666
+ "conditioned_number_segment",
32667
+ "cloze_gap_local"
32668
+ ]);
32669
+ var BROAD_EVIDENCE_KINDS = /* @__PURE__ */ new Set([
32670
+ "bm25_question_answer",
32671
+ "question_chunk_answer",
32672
+ "answer_chunk_question",
32673
+ "answer_window",
32674
+ "focused_answer_window",
32675
+ "shared_multi_segment"
32676
+ ]);
32677
+ var DIAGNOSTIC_BROAD_EVIDENCE_KINDS = /* @__PURE__ */ new Set([...BROAD_EVIDENCE_KINDS, "answer_directional_window"]);
32678
+ var NOISY_SHARED_EVIDENCE_KINDS = /* @__PURE__ */ new Set(["question_chunk_answer", "bm25_question_answer", "shared_multi_segment"]);
32679
+
32584
32680
  // src/predictor/text-utils.ts
32585
32681
  function nearestCueName(local, entries) {
32586
32682
  const center = Math.floor(local.length / 2);
@@ -32996,6 +33092,11 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
32996
33092
  "\u0434\u043E\u0437\u0430",
32997
33093
  "\u0434\u043E\u0437\u044B",
32998
33094
  "\u0434\u043E\u0437\u0435",
33095
+ "\u0440\u0435\u043A\u043E\u043C\u0435\u043D\u0434\u0443\u0435\u043C\u0430\u044F",
33096
+ "\u0440\u0435\u043A\u043E\u043C\u0435\u043D\u0434\u0443\u0435\u043C\u044B\u0439",
33097
+ "\u0440\u0435\u043A\u043E\u043C\u0435\u043D\u0434\u0443\u0435\u0442\u0441\u044F",
33098
+ "\u043D\u0430\u0437\u043D\u0430\u0447\u0430\u0435\u0442\u0441\u044F",
33099
+ "\u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435",
32999
33100
  "\u043F\u0440\u0438",
33000
33101
  "\u043B\u0435\u0447\u0435\u043D\u0438\u0438",
33001
33102
  "\u043B\u0435\u0447\u0435\u043D\u0438\u044F",
@@ -33059,7 +33160,7 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
33059
33160
  }
33060
33161
  function doseSlashNumbers(sourceText, drugTokens) {
33061
33162
  const out = [];
33062
- const slashPattern = /(\d+(?:[.,]\d+)?)\s*\/\s*(\d+(?:[.,]\d+)?)\s*мг/giu;
33163
+ const slashPattern = /(\d+(?:[.,]\d+)?)\s*\/\s*(\d+(?:[.,]\d+)?)\s*[\u006d\u043c]\u0433/giu;
33063
33164
  for (const match of sourceText.matchAll(slashPattern)) {
33064
33165
  const rawIndex = match.index ?? 0;
33065
33166
  const beforeText = sourceText.slice(Math.max(0, rawIndex - 150), rawIndex);
@@ -33088,7 +33189,7 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
33088
33189
  if (drugIndex < 0) return [];
33089
33190
  const local = normalized.slice(drugIndex, Math.min(normalized.length, drugIndex + 95));
33090
33191
  if (!containsNormalizedPhrase(local, "\u043C\u0433")) return [];
33091
- if (/\d+(?:[.,]\d+)?\s*\/\s*\d+(?:[.,]\d+)?\s*mг/iu.test(local)) return [];
33192
+ if (/\d+(?:[.,]\d+)?\s*\/\s*\d+(?:[.,]\d+)?\s*[\u006d\u043c][\u0433g]/iu.test(local)) return [];
33092
33193
  const firstNumber = local.match(/\d+(?:[.,]\d+)?/u);
33093
33194
  if (!firstNumber || (firstNumber.index ?? 0) > 55) return [];
33094
33195
  if (local.slice(0, firstNumber.index ?? 0).includes("+")) return [];
@@ -33102,6 +33203,21 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
33102
33203
  if (hasOtherDrugMarker) return [];
33103
33204
  return extractNumbers(local).slice(0, 2).map((number) => String(number).replace(",", "."));
33104
33205
  }
33206
+ function doseAssignedToDrugNumbers(sourceText, drugTokens) {
33207
+ const normalized = normalizeForSearch(sourceText);
33208
+ const out = [];
33209
+ const dosePattern = /(\d+(?:[.,]\d+)?)\s*[\u006d\u043c]\u0433/giu;
33210
+ for (const match of normalized.matchAll(dosePattern)) {
33211
+ const index = match.index ?? 0;
33212
+ if (normalized.slice(Math.max(0, index - 2), index).includes("/")) continue;
33213
+ const afterWindow = normalized.slice(index + match[0].length, Math.min(normalized.length, index + match[0].length + 70));
33214
+ const boundary = afterWindow.search(/[+.]|(?:^|\s)o\s|\s\d+(?:[.,]\d+)?\s*[\u0440p]\s*\/?\s*\u0434/u);
33215
+ const after = boundary >= 0 ? afterWindow.slice(0, boundary) : afterWindow;
33216
+ if (softCoverage(drugTokens, tokenizeNormalized(after)) < 0.8) continue;
33217
+ out.push(normalizeDoseNumber(match[1]));
33218
+ }
33219
+ return out;
33220
+ }
33105
33221
  function normalizeDoseNumber(value) {
33106
33222
  return String(value ?? "").replace(",", ".").replace(/\.0$/u, "");
33107
33223
  }
@@ -33120,12 +33236,22 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
33120
33236
  const normalized = normalizeForSearch(sourceText);
33121
33237
  const drugIndex = drugTokenIndex(normalized, drugTokens);
33122
33238
  if (drugIndex < 0) return [];
33239
+ const assignedNumbers = doseAssignedToDrugNumbers(sourceText, drugTokens);
33240
+ if (assignedNumbers.length) {
33241
+ return assignedNumbers.map((number) => ({ dose: number, doseRange: null, frequency: null }));
33242
+ }
33123
33243
  const local = normalized.slice(drugIndex, Math.min(normalized.length, drugIndex + 125));
33124
33244
  const facts = [];
33245
+ const slashNumbers = doseSlashNumbers(sourceText, drugTokens);
33246
+ if (slashNumbers.length) {
33247
+ return slashNumbers.map((number) => ({ dose: normalizeDoseNumber(number), doseRange: null, frequency: null }));
33248
+ }
33125
33249
  const dosePattern = /(\d+(?:[.,]\d+)?)(?:\s*-\s*(\d+(?:[.,]\d+)?))?\s*[\u006d\u043c]\u0433(?:\s*[\u0078\u0445]\s*(\d+(?:[.,]\d+)?))?/giu;
33126
33250
  for (const match of local.matchAll(dosePattern)) {
33127
33251
  const index = match.index ?? 0;
33128
33252
  if (index > 80) continue;
33253
+ const beforeNumber = local.slice(0, index).replace(/\s+$/u, "");
33254
+ if (beforeNumber.endsWith("/")) continue;
33129
33255
  facts.push({
33130
33256
  dose: normalizeDoseNumber(match[2] ?? match[1]),
33131
33257
  doseRange: match[2] ? [normalizeDoseNumber(match[1]), normalizeDoseNumber(match[2])] : null,
@@ -33133,7 +33259,7 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
33133
33259
  });
33134
33260
  break;
33135
33261
  }
33136
- for (const number of [...doseSlashNumbers(sourceText, drugTokens), ...doseNearDrugNumbers(sourceText, drugTokens)]) {
33262
+ for (const number of doseNearDrugNumbers(sourceText, drugTokens)) {
33137
33263
  facts.push({ dose: normalizeDoseNumber(number), doseRange: null, frequency: null });
33138
33264
  }
33139
33265
  return facts;
@@ -35272,6 +35398,220 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
35272
35398
  }
35273
35399
  return best;
35274
35400
  }
35401
+ function broadRecommendationQuestion(question) {
35402
+ const normalized = normalizeForSearch(question);
35403
+ return containsNormalizedPhrase(normalized, "\u0440\u0435\u043A\u043E\u043C\u0435\u043D\u0434") || containsNormalizedPhrase(normalized, "\u043D\u0430\u0437\u043D\u0430\u0447") || containsNormalizedPhrase(normalized, "\u043F\u0440\u043E\u0432\u043E\u0434") || containsNormalizedPhrase(normalized, "\u043B\u0435\u0447\u0435\u043D") || containsNormalizedPhrase(normalized, "\u0442\u0435\u0440\u0430\u043F") || containsNormalizedPhrase(normalized, "\u043F\u0440\u043E\u0444\u0438\u043B\u0430\u043A\u0442");
35404
+ }
35405
+ function recommendationCueSegment(segmentNorm) {
35406
+ return containsNormalizedPhrase(segmentNorm, "\u0440\u0435\u043A\u043E\u043C\u0435\u043D\u0434") || containsNormalizedPhrase(segmentNorm, "\u043D\u0430\u0437\u043D\u0430\u0447") || containsNormalizedPhrase(segmentNorm, "\u043F\u0440\u043E\u0432\u043E\u0434") || containsNormalizedPhrase(segmentNorm, "\u043F\u0440\u0438\u043C\u0435\u043D");
35407
+ }
35408
+ function bestRecommendationBlockSupport({ mode, pages, topQuestionPages, question, answer, answerTokens }) {
35409
+ if (mode !== "multi") return null;
35410
+ if (!broadRecommendationQuestion(question)) return null;
35411
+ const questionNorm = normalizeForSearch(question);
35412
+ if (containsNormalizedPhrase(questionNorm, "\u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435")) return null;
35413
+ const qTokens = recommendationQuestionTokens(question);
35414
+ if (qTokens.length < 3) return null;
35415
+ let best = null;
35416
+ for (const segment of recommendationSegments(pages)) {
35417
+ if (topQuestionPages?.size && !topQuestionPages.has(segment.page)) continue;
35418
+ if (!recommendationCueSegment(segment.normalized)) continue;
35419
+ if (recommendationPresenceMismatch(answer.text, segment.normalized)) continue;
35420
+ const segmentTokens = tokenizeNormalized(segment.normalized);
35421
+ const qCoverage = recommendationQuestionCoverage(questionNorm, qTokens, segment.normalized);
35422
+ const qHits = tokenHitCount(qTokens, segmentTokens);
35423
+ if (qCoverage < 0.54 || qHits < Math.min(4, qTokens.length)) continue;
35424
+ const answerHit = recommendationSegmentAnswerHit(answer, answerTokens, segment.normalized, segmentTokens);
35425
+ if (!answerHit.supportHit) continue;
35426
+ const score = 12.4 + Math.min(1, qCoverage) * 4.6 + Math.min(5, qHits) * 0.7 + answerHit.answerCoverage * 4.8 + answerHit.numericCoverage * 1.8 + (answerHit.strongPhraseHit ? 2.4 : answerHit.phraseHit ? 1.2 : 0);
35427
+ best = betterEvidence(best, {
35428
+ answerId: answer.id,
35429
+ page: segment.page,
35430
+ text: segment.text,
35431
+ score,
35432
+ kind: "recommendation_block_segment"
35433
+ });
35434
+ }
35435
+ return best;
35436
+ }
35437
+
35438
+ // src/predictor/scorers/option-family.ts
35439
+ var LESS_CUES = ["\u0434\u043E", "\u043C\u0435\u043D\u0435\u0435", "\u043C\u0435\u043D\u044C\u0448\u0435", "\u043D\u0438\u0436\u0435", "\u043C\u043E\u043B\u043E\u0436\u0435"].map(
35440
+ (item) => normalizeForSearch(item)
35441
+ );
35442
+ var GREATER_CUES = ["\u0431\u043E\u043B\u0435\u0435", "\u0431\u043E\u043B\u044C\u0448\u0435", "\u0432\u044B\u0448\u0435", "\u0441\u0432\u044B\u0448\u0435", "\u0441\u0442\u0430\u0440\u0448\u0435"].map(
35443
+ (item) => normalizeForSearch(item)
35444
+ );
35445
+ var COMBO_QUESTION_CUES = [
35446
+ "\u0440\u0435\u043A\u043E\u043C\u0435\u043D\u0434",
35447
+ "\u043D\u0430\u0437\u043D\u0430\u0447",
35448
+ "\u043F\u0440\u0438\u043C\u0435\u043D",
35449
+ "\u0442\u0435\u0440\u0430\u043F",
35450
+ "\u043B\u0435\u0447\u0435\u043D"
35451
+ ].map((item) => normalizeForSearch(item));
35452
+ var COMBO_GENERIC_TOKENS = new Set(
35453
+ [
35454
+ "\u043C\u043E\u043D\u043E\u0442\u0435\u0440\u0430\u043F\u0438\u044F",
35455
+ "\u043C\u043E\u043D\u043E\u0442\u0435\u0440\u0430\u043F\u0438\u044E",
35456
+ "\u043A\u043E\u043C\u0431\u0438\u043D\u0430\u0446\u0438\u044F",
35457
+ "\u043A\u043E\u043C\u0431\u0438\u043D\u0430\u0446\u0438\u044E",
35458
+ "\u0438\u043B\u0438",
35459
+ "\u0433\u0440\u0443\u043F\u043F",
35460
+ "\u043F\u0440\u0435\u043F\u0430\u0440\u0430\u0442",
35461
+ "\u043D\u0430\u0437\u043D\u0430\u0447",
35462
+ "\u043F\u0440\u0438\u043C\u0435\u043D"
35463
+ ].flatMap((item) => uniqueTokens(item))
35464
+ );
35465
+ var COMBO_UNIT_PARTS = /* @__PURE__ */ new Set(["m\u0433", "m\u043B", "k\u0433", "me", "mm", "m\u0438h", "cyt", "\u0434h", "pa\u0437", "mg", "ml", "kg"]);
35466
+ function answerComparatorSpecs(answerText) {
35467
+ const normalized = normalizeForSearch(answerText);
35468
+ const numbers = extractNumbers(answerText).map((number) => number.replace(",", "."));
35469
+ const specs = [];
35470
+ for (const number of numbers) {
35471
+ const escaped = number.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
35472
+ const lessPattern = new RegExp(`(?:<|\\b(?:${LESS_CUES.join("|")})\\b\\s*)${escaped}(?:\\b|\\s|%|$)`, "iu");
35473
+ const greaterPattern = new RegExp(`(?:>|\\b(?:${GREATER_CUES.join("|")})\\b\\s*)${escaped}(?:\\b|\\s|%|$)`, "iu");
35474
+ if (lessPattern.test(normalized)) specs.push({ number, direction: "less" });
35475
+ if (greaterPattern.test(normalized)) specs.push({ number, direction: "greater" });
35476
+ }
35477
+ return specs;
35478
+ }
35479
+ function opposite(direction) {
35480
+ return direction === "less" ? "greater" : "less";
35481
+ }
35482
+ function answerFamilyHasOppositeComparator(answer, answers, spec) {
35483
+ return answers.some((candidate) => {
35484
+ if (candidate.id === answer.id) return false;
35485
+ return answerComparatorSpecs(candidate.text).some((candidateSpec) => candidateSpec.number === spec.number && candidateSpec.direction === opposite(spec.direction));
35486
+ });
35487
+ }
35488
+ function sourceDirectionsForNumber(text, number) {
35489
+ const normalized = normalizeForSearch(text).replace(/\u2264/gu, "<").replace(/\u2265/gu, ">");
35490
+ const escaped = number.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
35491
+ const directions = /* @__PURE__ */ new Set();
35492
+ const pattern = new RegExp(escaped, "giu");
35493
+ for (const match of normalized.matchAll(pattern)) {
35494
+ const index = match.index ?? 0;
35495
+ const before = normalized.slice(Math.max(0, index - 26), index).trim();
35496
+ const after = normalized.slice(index + match[0].length, Math.min(normalized.length, index + match[0].length + 12));
35497
+ const local = `${before} ${match[0]} ${after}`;
35498
+ const previous = before.split(/\s+/u).filter(Boolean).slice(-3).join(" ");
35499
+ if (/[<]\s*$/u.test(before) || LESS_CUES.some((cue) => containsNormalizedPhrase(previous, cue))) directions.add("less");
35500
+ if (/[>]\s*$/u.test(before) || GREATER_CUES.some((cue) => containsNormalizedPhrase(previous, cue))) directions.add("greater");
35501
+ if (containsNormalizedPhrase(local, normalizeForSearch(`\u043D\u0435 \u043C\u0435\u043D\u0435\u0435 ${number}`))) directions.add("greater");
35502
+ if (containsNormalizedPhrase(local, normalizeForSearch(`\u043D\u0435 \u0431\u043E\u043B\u0435\u0435 ${number}`))) directions.add("less");
35503
+ }
35504
+ return directions;
35505
+ }
35506
+ function optionFamilyComparatorAdjustment({
35507
+ answer,
35508
+ answers,
35509
+ evidence
35510
+ }) {
35511
+ const answerSpecs = answerComparatorSpecs(answer.text);
35512
+ if (answerSpecs.length !== 1) return { adjustment: 0, evidence: null };
35513
+ const specs = answerSpecs.filter((spec) => answerFamilyHasOppositeComparator(answer, answers, spec));
35514
+ if (!specs.length) return { adjustment: 0, evidence: null };
35515
+ for (const item of evidence) {
35516
+ if ((item.score ?? 0) < 5.5 || !item.text) continue;
35517
+ for (const spec of specs) {
35518
+ const sourceDirections = sourceDirectionsForNumber(item.text, spec.number);
35519
+ if (!sourceDirections.size || sourceDirections.has(spec.direction)) continue;
35520
+ if (!sourceDirections.has(opposite(spec.direction))) continue;
35521
+ return {
35522
+ adjustment: -4.2,
35523
+ evidence: {
35524
+ answerId: answer.id,
35525
+ page: item.page,
35526
+ text: item.text,
35527
+ score: Math.max(6.8, Math.min(12.5, item.score)),
35528
+ kind: "option_family_comparator_mismatch"
35529
+ }
35530
+ };
35531
+ }
35532
+ }
35533
+ return { adjustment: 0, evidence: null };
35534
+ }
35535
+ function compactComboPhrases(answerText) {
35536
+ const normalized = normalizeForSearch(answerText);
35537
+ const phrases = /* @__PURE__ */ new Set();
35538
+ for (const match of normalized.matchAll(/[a-z\u0430-\u044f0-9]{2,}(?:[+/][a-z\u0430-\u044f0-9]{2,})+/giu)) {
35539
+ if (validCompactComboPhrase(match[0])) phrases.add(match[0]);
35540
+ }
35541
+ return [...phrases].filter((phrase) => phrase.length >= 5);
35542
+ }
35543
+ function validCompactComboPhrase(phrase) {
35544
+ const parts = phrase.split(/[+/]/u).filter(Boolean);
35545
+ if (parts.length < 2) return false;
35546
+ return parts.every((part) => part.length >= 3 && part.length <= 10 && !COMBO_UNIT_PARTS.has(part) && !/^\d/u.test(part));
35547
+ }
35548
+ function comboQuestion(question) {
35549
+ const normalized = normalizeForSearch(question);
35550
+ return COMBO_QUESTION_CUES.some((cue) => normalized.includes(cue));
35551
+ }
35552
+ function alternativeComboTokens(answerText) {
35553
+ const normalized = normalizeForSearch(answerText);
35554
+ const alternative = containsNormalizedPhrase(normalized, "\u0438\u043B\u0438") || containsNormalizedPhrase(normalized, "\u043C\u043E\u043D\u043E\u0442\u0435\u0440\u0430\u043F");
35555
+ if (!alternative) return [];
35556
+ return uniqueTokens(answerText).filter((token) => token.length >= 3 && !/^\d/u.test(token) && !COMBO_GENERIC_TOKENS.has(token)).slice(0, 8);
35557
+ }
35558
+ function evidenceHasCompactTokenPair(text, tokens) {
35559
+ const normalized = normalizeForSearch(text);
35560
+ if (!/[+/]/u.test(normalized)) return false;
35561
+ for (let leftIndex = 0; leftIndex < tokens.length - 1; leftIndex += 1) {
35562
+ for (let rightIndex = leftIndex + 1; rightIndex < tokens.length; rightIndex += 1) {
35563
+ const left = tokens[leftIndex];
35564
+ const right = tokens[rightIndex];
35565
+ const leftHit = normalized.indexOf(left);
35566
+ const rightHit = normalized.indexOf(right);
35567
+ if (leftHit < 0 || rightHit < 0) continue;
35568
+ const start = Math.min(leftHit, rightHit);
35569
+ const end = Math.max(leftHit + left.length, rightHit + right.length);
35570
+ if (end - start > 56) continue;
35571
+ const local = normalized.slice(start, end);
35572
+ if (/[+/]/u.test(local) && !containsNormalizedPhrase(local, "\u0438\u043B\u0438")) return true;
35573
+ }
35574
+ }
35575
+ return false;
35576
+ }
35577
+ function optionFamilyCompactComboAdjustment({
35578
+ question,
35579
+ answer,
35580
+ evidence
35581
+ }) {
35582
+ if (!comboQuestion(question)) return { adjustment: 0, evidence: null };
35583
+ const comboPhrases = compactComboPhrases(answer.text);
35584
+ const alternativeTokens = alternativeComboTokens(answer.text);
35585
+ for (const item of evidence) {
35586
+ if ((item.score ?? 0) < 5.5 || !item.text) continue;
35587
+ const normalizedEvidence = normalizeForSearch(item.text);
35588
+ if (comboPhrases.some((phrase) => normalizedEvidence.includes(phrase))) {
35589
+ return {
35590
+ adjustment: 6.8,
35591
+ evidence: {
35592
+ answerId: answer.id,
35593
+ page: item.page,
35594
+ text: item.text,
35595
+ score: Math.max(9.2, Math.min(14.5, item.score + 1.1)),
35596
+ kind: "option_family_compact_combo_match"
35597
+ }
35598
+ };
35599
+ }
35600
+ if (alternativeTokens.length >= 2 && evidenceHasCompactTokenPair(item.text, alternativeTokens)) {
35601
+ return {
35602
+ adjustment: -6.4,
35603
+ evidence: {
35604
+ answerId: answer.id,
35605
+ page: item.page,
35606
+ text: item.text,
35607
+ score: Math.max(7.2, Math.min(13, item.score)),
35608
+ kind: "option_family_compact_combo_mismatch"
35609
+ }
35610
+ };
35611
+ }
35612
+ }
35613
+ return { adjustment: 0, evidence: null };
35614
+ }
35275
35615
 
35276
35616
  // src/predictor/scorers/direction.ts
35277
35617
  var POLARITY_UP_CUES = ["\u043F\u043E\u0432\u044B\u0448", "\u0443\u0432\u0435\u043B\u0438\u0447", "\u0432\u043E\u0437\u0440\u0430\u0441\u0442\u0430", "\u0440\u043E\u0441\u0442", "\u0432\u044B\u0441\u043E\u043A", "\u0431\u043E\u043B\u0435\u0435", "\u0432\u044B\u0448\u0435"].map((item) => normalizeForSearch(item));
@@ -35383,6 +35723,117 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
35383
35723
  if (bestMismatch && (!bestMatch || bestMismatch.score > bestMatch.score + 0.4)) return { support: null, adjustment: -4.8, evidence: bestMismatch };
35384
35724
  return { support: null, adjustment: 0, evidence: null };
35385
35725
  }
35726
+ var CLINICAL_COURSE_CUE_GROUPS = [
35727
+ {
35728
+ target: "\u043E\u0441\u0442\u0440",
35729
+ opposite: ["\u0445\u0440\u043E\u043D", "\u0445\u0440\u043E\u043D\u0438\u0447"]
35730
+ },
35731
+ {
35732
+ target: "\u0445\u0440\u043E\u043D",
35733
+ opposite: ["\u043E\u0441\u0442\u0440"]
35734
+ }
35735
+ ].map((group) => ({
35736
+ target: normalizeForSearch(group.target),
35737
+ opposite: group.opposite.map((item) => normalizeForSearch(item))
35738
+ }));
35739
+ function clinicalCourseCue(question) {
35740
+ const tokens = tokenize(question);
35741
+ for (const group of CLINICAL_COURSE_CUE_GROUPS) {
35742
+ if (tokens.some((token) => token.startsWith(group.target))) return group;
35743
+ }
35744
+ return null;
35745
+ }
35746
+ function clinicalCourseRelationQuestion(question) {
35747
+ const normalized = normalizeForSearch(question);
35748
+ if (containsNormalizedPhrase(normalized, "\u0440\u0435\u043A\u043E\u043C\u0435\u043D\u0434") || containsNormalizedPhrase(normalized, "\u043D\u0430\u0437\u043D\u0430\u0447")) {
35749
+ return false;
35750
+ }
35751
+ return [
35752
+ "\u043F\u0440\u043E\u044F\u0432\u043B",
35753
+ "\u0445\u0430\u0440\u0430\u043A\u0442\u0435\u0440",
35754
+ "\u0442\u0438\u043F\u0438\u0447",
35755
+ "\u043F\u0440\u0438\u0437\u043D\u0430\u043A",
35756
+ "\u0441\u0438\u043C\u043F\u0442\u043E\u043C"
35757
+ ].some((cue) => normalized.includes(normalizeForSearch(cue)));
35758
+ }
35759
+ var CLINICAL_COURSE_GENERIC_BINDING_TOKENS = new Set(
35760
+ [
35761
+ "\u0442\u0435\u0447\u0435\u043D\u0438\u0435",
35762
+ "\u0442\u0435\u0447\u0435\u043D\u0438\u044F",
35763
+ "\u0444\u043E\u0440\u043C\u0430",
35764
+ "\u0444\u043E\u0440\u043C\u044B",
35765
+ "\u0431\u043E\u043B\u0435\u0437\u043D\u044C",
35766
+ "\u0431\u043E\u043B\u0435\u0437\u043D\u0438",
35767
+ "\u0433\u0435\u043F\u0430\u0442\u0438\u0442",
35768
+ "\u0441\u0438\u043C\u043F\u0442\u043E\u043C",
35769
+ "\u043F\u0440\u0438\u0437\u043D\u0430\u043A",
35770
+ "\u043F\u0440\u043E\u044F\u0432\u043B\u0435\u043D\u0438\u0435",
35771
+ "\u043F\u0440\u043E\u044F\u0432\u043B\u044F\u0442\u044C"
35772
+ ].flatMap((item) => tokenize(item))
35773
+ );
35774
+ function clinicalCourseBindingTokens(answerTokens, questionTokens) {
35775
+ const questionSet = new Set(questionTokens);
35776
+ return answerTokens.filter(
35777
+ (token) => token.length >= 4 && /[а-яё]/u.test(token) && !FOCUS_STOPWORDS.has(token) && !questionSet.has(token) && !CLINICAL_COURSE_GENERIC_BINDING_TOKENS.has(token)
35778
+ );
35779
+ }
35780
+ function nearestClinicalCourseCueBeforeAnswer(tokens, bindingTokens, cueGroup) {
35781
+ let best = null;
35782
+ if (!bindingTokens.length) return null;
35783
+ for (let index = 0; index < tokens.length; index += 1) {
35784
+ const token = tokens[index];
35785
+ if (!bindingTokens.some((answerToken) => token === answerToken || token.startsWith(answerToken) || answerToken.startsWith(token))) continue;
35786
+ const start = Math.max(0, index - 10);
35787
+ for (let cursor = index - 1; cursor >= start; cursor -= 1) {
35788
+ const beforeToken = tokens[cursor];
35789
+ const distance = index - cursor;
35790
+ let type = null;
35791
+ if (beforeToken.startsWith(cueGroup.target)) type = "target";
35792
+ else if (cueGroup.opposite.some((opposite2) => beforeToken.startsWith(opposite2))) type = "opposite";
35793
+ if (!type) continue;
35794
+ if (!best || distance < best.distance) best = { type, distance };
35795
+ break;
35796
+ }
35797
+ }
35798
+ return best;
35799
+ }
35800
+ function clinicalCourseCueAdjustment({ mode, pages, topQuestionPages, question, questionTokens, focusTokens, answer, answerTokens }) {
35801
+ if (mode !== "single") return { support: null, adjustment: 0, evidence: null };
35802
+ if (!clinicalCourseRelationQuestion(question)) return { support: null, adjustment: 0, evidence: null };
35803
+ const cueGroup = clinicalCourseCue(question);
35804
+ if (!cueGroup) return { support: null, adjustment: 0, evidence: null };
35805
+ const bindingTokens = clinicalCourseBindingTokens(answerTokens, questionTokens);
35806
+ if (!bindingTokens.length) return { support: null, adjustment: 0, evidence: null };
35807
+ const usefulFocus = focusTokens?.length ? focusTokens : questionTokens;
35808
+ let bestMatch = null;
35809
+ let bestMismatch = null;
35810
+ for (const page of pages) {
35811
+ const topPage = topQuestionPages?.has(page.page);
35812
+ const adjacentTopPage = topQuestionPages?.has(page.page - 1) || topQuestionPages?.has(page.page + 1);
35813
+ if (topQuestionPages?.size && !topPage && !adjacentTopPage) continue;
35814
+ for (const segment of cachedLineWindowSegments(page)) {
35815
+ const focusCoverage = coverage(usefulFocus, segment.tokens);
35816
+ if (focusCoverage < 0.12) continue;
35817
+ const binding = nearestClinicalCourseCueBeforeAnswer(segment.tokens, bindingTokens, cueGroup);
35818
+ if (!binding) continue;
35819
+ const answerHits = tokenHitCount(bindingTokens, segment.tokens);
35820
+ if (answerHits < Math.min(2, bindingTokens.length)) continue;
35821
+ const score = 9.2 + Math.min(0.5, focusCoverage) * 5 + Math.min(3, answerHits) * 0.7 + Math.max(0, 10 - binding.distance) * 0.12;
35822
+ const evidence = {
35823
+ answerId: answer.id,
35824
+ page: page.page,
35825
+ text: segment.text,
35826
+ score,
35827
+ kind: binding.type === "target" ? "clinical_course_cue_segment" : "clinical_course_cue_mismatch"
35828
+ };
35829
+ if (binding.type === "target") bestMatch = betterEvidence(bestMatch, evidence);
35830
+ else bestMismatch = betterEvidence(bestMismatch, evidence);
35831
+ }
35832
+ }
35833
+ if (bestMatch && (!bestMismatch || bestMatch.score >= bestMismatch.score - 0.6)) return { support: bestMatch, adjustment: 0, evidence: null };
35834
+ if (bestMismatch && (!bestMatch || bestMismatch.score > bestMatch.score + 0.6)) return { support: null, adjustment: -6.8, evidence: bestMismatch };
35835
+ return { support: null, adjustment: 0, evidence: null };
35836
+ }
35386
35837
  var CONTRAST_CUE_GROUPS = [
35387
35838
  {
35388
35839
  answer: ["\u0432\u0435\u0440\u0445\u043D"],
@@ -36885,52 +37336,6 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
36885
37336
  }
36886
37337
 
36887
37338
  // src/predictor/selection.ts
36888
- var STRUCTURAL_EVIDENCE_WEIGHTS = new Map(
36889
- Object.entries({
36890
- coordinate_table_row: 1.25,
36891
- coordinate_table_group: 1.25,
36892
- coordinate_table_group_inverse: 1.25,
36893
- coordinate_table_multicell_row: 1.25,
36894
- coordinate_table_membership: 1.15,
36895
- parenthetical_group_segment: 1.05,
36896
- preceding_question_label: 1.05,
36897
- question_continuation_list: 1.05,
36898
- exact_numeric_option_segment: 1.05,
36899
- exact_hour_alias_segment: 1.05,
36900
- short_medical_alias_segment: 0.9,
36901
- visual_table_column: 1.2,
36902
- exact_short_label_visual_row: 1.15,
36903
- short_label_visual_row: 1.05,
36904
- answer_ordinal_row: 1.05,
36905
- fibrosis_stage_row: 1.2,
36906
- gene_sentence_segment: 1.1,
36907
- clinical_feature_segment: 1,
36908
- mkb_class_exclusion_absent: 1,
36909
- classification_code_segment: 1.15,
36910
- label_number_proximity: 1,
36911
- label_definition_segment: 1,
36912
- row_label_segment: 0.95,
36913
- bounded_list_segment: 0.95,
36914
- ordinal_list_segment: 0.9,
36915
- drug_dose_segment: 0.9,
36916
- recommendation_item_segment: 0.85,
36917
- explicit_recommendation_target_segment: 0.85,
36918
- numeric_condition_less_than: 0.85,
36919
- numeric_condition_more_than: 0.85,
36920
- numeric_condition_equal: 0.85,
36921
- conditioned_number_segment: 0.8,
36922
- cloze_gap_local: 0.8
36923
- })
36924
- );
36925
- var BROAD_EVIDENCE_KINDS = /* @__PURE__ */ new Set([
36926
- "bm25_question_answer",
36927
- "question_chunk_answer",
36928
- "answer_chunk_question",
36929
- "answer_window",
36930
- "focused_answer_window",
36931
- "shared_multi_segment"
36932
- ]);
36933
- var NOISY_SHARED_EVIDENCE_KINDS = /* @__PURE__ */ new Set(["question_chunk_answer", "bm25_question_answer", "shared_multi_segment"]);
36934
37339
  function calibrateScores(answerScores) {
36935
37340
  const rawValues = answerScores.map((item) => item.raw);
36936
37341
  const max = Math.max(...rawValues, 1e-4);
@@ -37031,7 +37436,7 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
37031
37436
  bestScore = score;
37032
37437
  bestKind = evidence.kind;
37033
37438
  }
37034
- const weight = STRUCTURAL_EVIDENCE_WEIGHTS.get(evidence.kind) ?? 0;
37439
+ const weight = SELECTION_STRUCTURAL_EVIDENCE_WEIGHTS.get(evidence.kind) ?? 0;
37035
37440
  if (weight > 0) {
37036
37441
  bestStructuralScore = Math.max(bestStructuralScore, score);
37037
37442
  structuralWeight = Math.max(structuralWeight, weight);
@@ -38963,6 +39368,256 @@ ${nextPage.text}`;
38963
39368
  }
38964
39369
  return best;
38965
39370
  }
39371
+ function definitionQuestionLike(question) {
39372
+ const normalized = normalizeForSearch(question);
39373
+ return questionDefinitionTerm(question) || containsNormalizedPhrase(normalized, "\u044D\u0442\u043E") || containsNormalizedPhrase(normalized, "\u043F\u043E\u043D\u0438\u043C\u0430") || containsNormalizedPhrase(normalized, "\u043D\u0430\u0437\u044B\u0432\u0430");
39374
+ }
39375
+ function definitionCueWindow(normalized) {
39376
+ return containsNormalizedPhrase(normalized, "\u044D\u0442\u043E") || containsNormalizedPhrase(normalized, "\u043F\u043E\u043D\u0438\u043C\u0430") || containsNormalizedPhrase(normalized, "\u043D\u0430\u0437\u044B\u0432\u0430") || /(?:^|\s)[a-z\u0430-\u044f]{4,}\s+[-\u2013\u2014]\s+/iu.test(normalized);
39377
+ }
39378
+ function definitionExactFragments(text) {
39379
+ const fragments = String(text ?? "").split(/(?<=[.!?;])\s+/u).map((item) => item.trim()).filter((item) => item.length >= 20);
39380
+ return fragments.length ? fragments : [String(text ?? "")];
39381
+ }
39382
+ var DEFINITION_TERM_GENERIC_TOKENS = new Set(
39383
+ [
39384
+ "\u043F\u043E\u0434",
39385
+ "\u043F\u043E\u043D\u0438\u043C\u0430\u044E\u0442",
39386
+ "\u043F\u043E\u043D\u0438\u043C\u0430\u0435\u0442\u0441\u044F",
39387
+ "\u044D\u0442\u043E",
39388
+ "\u0441\u0447\u0438\u0442\u0430\u0435\u0442\u0441\u044F",
39389
+ "\u043F\u0440\u0438\u0437\u043D\u0430\u043A"
39390
+ ].flatMap((item) => uniqueTokens(item))
39391
+ );
39392
+ function primaryDefinitionTermToken(question) {
39393
+ const term = questionDefinitionTerm(question);
39394
+ const tokens = uniqueTokens(term ?? question).filter((token) => token.length >= 4 && !DEFINITION_TERM_GENERIC_TOKENS.has(token));
39395
+ return tokens[0] ?? "";
39396
+ }
39397
+ function editDistanceAtMostOne(left, right) {
39398
+ if (left === right) return true;
39399
+ if (Math.abs(left.length - right.length) > 1) return false;
39400
+ let edits = 0;
39401
+ let i = 0;
39402
+ let j = 0;
39403
+ while (i < left.length && j < right.length) {
39404
+ if (left[i] === right[j]) {
39405
+ i += 1;
39406
+ j += 1;
39407
+ continue;
39408
+ }
39409
+ edits += 1;
39410
+ if (edits > 1) return false;
39411
+ if (left.length > right.length) i += 1;
39412
+ else if (right.length > left.length) j += 1;
39413
+ else {
39414
+ i += 1;
39415
+ j += 1;
39416
+ }
39417
+ }
39418
+ return edits + (left.length - i) + (right.length - j) <= 1;
39419
+ }
39420
+ function definitionFragmentMatchesQuestionTerm(fragmentTokens, primaryTerm) {
39421
+ if (!primaryTerm) return true;
39422
+ const cueIndex = fragmentTokens.findIndex((token) => token === normalizeForSearch("\u044D\u0442\u043E"));
39423
+ const labelTokens = fragmentTokens.slice(0, cueIndex >= 0 ? cueIndex : Math.min(3, fragmentTokens.length));
39424
+ return labelTokens.some((token) => token.length >= 4 && editDistanceAtMostOne(token, primaryTerm));
39425
+ }
39426
+ function bestDefinitionExactAnswerSupport({ mode, pages, topQuestionPages, question, answer, answerTokens }) {
39427
+ if (mode !== "single" || !definitionQuestionLike(question)) return null;
39428
+ const answerPhrases = answerSearchPhrases(answer.text).slice(0, 12);
39429
+ const primaryTerm = primaryDefinitionTermToken(question);
39430
+ let best = null;
39431
+ for (const page of pages) {
39432
+ if (topQuestionPages?.size && !topQuestionPages.has(page.page) && !topQuestionPages.has(page.page - 1) && !topQuestionPages.has(page.page + 1)) {
39433
+ continue;
39434
+ }
39435
+ for (const source of cachedLineWindowSegments(page)) {
39436
+ for (const fragment of definitionExactFragments(source.text)) {
39437
+ const normalized = normalizeForSearch(fragment);
39438
+ if (!definitionCueWindow(normalized)) continue;
39439
+ const fragmentTokens = tokenizeNormalized(normalized);
39440
+ if (!definitionFragmentMatchesQuestionTerm(fragmentTokens, primaryTerm)) continue;
39441
+ const phraseHit = answerPhrases.some((phrase) => containsNormalizedPhrase(normalized, phrase));
39442
+ const answerCoverage = strictSoftCoverage(answerTokens, fragmentTokens);
39443
+ if (!phraseHit || answerCoverage < 0.72) continue;
39444
+ const score = 15.2 + answerCoverage * 6 + numberCoverage(answer.text, normalized) * 0.8;
39445
+ best = betterEvidence(best, {
39446
+ answerId: answer.id,
39447
+ page: page.page,
39448
+ text: fragment,
39449
+ score,
39450
+ kind: "definition_exact_answer_segment"
39451
+ });
39452
+ }
39453
+ }
39454
+ }
39455
+ return best;
39456
+ }
39457
+ var DEFINITION_COMPLETION_EVIDENCE = /* @__PURE__ */ new Set(["definition_exact_answer_segment", "term_definition_segment", "label_definition_segment"]);
39458
+ function definitionCompletionAdjustment({ mode, question, answer, answers, answerTokens }, evidence) {
39459
+ if (mode !== "single" || !definitionQuestionLike(question)) return { adjustment: 0, evidence: null };
39460
+ const definitionEvidence = evidence.find((item) => DEFINITION_COMPLETION_EVIDENCE.has(item.kind) && (item.score ?? 0) >= 18);
39461
+ if (!definitionEvidence) return { adjustment: 0, evidence: null };
39462
+ const answerNorm = normalizeForSearch(answer.text);
39463
+ if (answerTokens.length < 5 || answerNorm.length < 32) return { adjustment: 0, evidence: null };
39464
+ let contained = 0;
39465
+ for (const candidate of answers) {
39466
+ if (candidate.id === answer.id) continue;
39467
+ const candidateNorm = normalizeForSearch(candidate.text);
39468
+ if (candidateNorm.length < 18 || candidateNorm === answerNorm) continue;
39469
+ const candidateTokens = uniqueTokens(candidate.text);
39470
+ if (candidateTokens.length < 3 || candidateTokens.length >= answerTokens.length) continue;
39471
+ if (answerNorm.includes(candidateNorm)) contained += 1;
39472
+ }
39473
+ if (!contained) return { adjustment: 0, evidence: null };
39474
+ const adjustment = Math.min(5, contained * 2.4);
39475
+ return {
39476
+ adjustment,
39477
+ evidence: {
39478
+ answerId: answer.id,
39479
+ page: definitionEvidence.page,
39480
+ text: definitionEvidence.text,
39481
+ score: Math.max(8.5, Math.min(14.5, definitionEvidence.score)),
39482
+ kind: "definition_completion_specificity"
39483
+ }
39484
+ };
39485
+ }
39486
+ var FREQUENCY_POLARITY_HIGH_CUES = [
39487
+ "\u043D\u0430\u0438\u0431\u043E\u043B\u0435\u0435 \u0447\u0430\u0441\u0442",
39488
+ "\u0441\u0430\u043C\u043E\u0439 \u0447\u0430\u0441\u0442",
39489
+ "\u0441\u0430\u043C\u044B\u043C \u0447\u0430\u0441\u0442",
39490
+ "\u0447\u0430\u0441\u0442\u043E \u0432\u0441\u0442\u0440\u0435\u0447",
39491
+ "\u0447\u0430\u0449\u0435",
39492
+ "\u0432\u0435\u0434\u0443\u0449"
39493
+ ];
39494
+ var FREQUENCY_POLARITY_LOW_CUES = [
39495
+ "\u0440\u0435\u0434\u043A",
39496
+ "\u0440\u0435\u0436\u0435"
39497
+ ];
39498
+ var FREQUENCY_POLARITY_GENERIC_FOCUS = new Set(
39499
+ [
39500
+ "\u043D\u0430\u0438\u0431\u043E\u043B\u0435\u0435",
39501
+ "\u0447\u0430\u0441\u0442\u044B\u0439",
39502
+ "\u0447\u0430\u0441\u0442\u0430\u044F",
39503
+ "\u0447\u0430\u0441\u0442\u043E\u0439",
39504
+ "\u0447\u0430\u0441\u0442\u043E\u0435",
39505
+ "\u0440\u0435\u0434\u043A\u0438\u0439",
39506
+ "\u0440\u0435\u0434\u043A\u0430\u044F",
39507
+ "\u0440\u0435\u0434\u043A\u043E\u0439",
39508
+ "\u0444\u043E\u0440\u043C\u0430",
39509
+ "\u0444\u043E\u0440\u043C\u043E\u0439",
39510
+ "\u0432\u0430\u0440\u0438\u0430\u043D\u0442",
39511
+ "\u0432\u0430\u0440\u0438\u0430\u043D\u0442\u043E\u043C",
39512
+ "\u0440\u043E\u043B\u044C",
39513
+ "\u043E\u0442\u0432\u043E\u0434\u0438\u0442\u0441\u044F",
39514
+ "\u0432\u0441\u0442\u0440\u0435\u0447\u0430\u0435\u0442\u0441\u044F"
39515
+ ].flatMap((item) => uniqueTokens(item))
39516
+ );
39517
+ function frequencyPolarity(normalized) {
39518
+ if (FREQUENCY_POLARITY_LOW_CUES.some((cue) => containsNormalizedPhrase(normalized, cue))) return "low";
39519
+ if (FREQUENCY_POLARITY_HIGH_CUES.some((cue) => containsNormalizedPhrase(normalized, cue))) return "high";
39520
+ return null;
39521
+ }
39522
+ function frequencyPolarityFocusTokens(focusTokens, answerTokens) {
39523
+ const answerSet = new Set(answerTokens ?? []);
39524
+ return (focusTokens ?? []).filter((token) => token.length >= 4 && !answerSet.has(token) && !FREQUENCY_POLARITY_GENERIC_FOCUS.has(token));
39525
+ }
39526
+ function containsPhraseOutsideParentheses(text, phrases) {
39527
+ const normalized = normalizeForSearch(String(text ?? "").replace(/\([^)]*\)/gu, " "));
39528
+ for (const phrase of phrases) {
39529
+ const normalizedPhrase = normalizeForSearch(phrase);
39530
+ if (!normalizedPhrase) continue;
39531
+ if (normalized.includes(normalizedPhrase)) return true;
39532
+ }
39533
+ return false;
39534
+ }
39535
+ function frequencyPolarityFragments(text) {
39536
+ const fragments = String(text ?? "").split(/(?<=[.!?;])\s+/u).map((item) => item.trim()).filter((item) => item.length >= 20);
39537
+ return fragments.length ? fragments : [String(text ?? "")];
39538
+ }
39539
+ function frequencyListItemLine(line) {
39540
+ return /^\s*(?:[•*\-]|\d+[.)]|[IVX]+[.)])\s+/iu.test(String(line ?? ""));
39541
+ }
39542
+ function frequencyPolarityListItems(pages, pageIndex, lineIndex) {
39543
+ const items = [];
39544
+ for (let offset = 0; offset <= 1; offset += 1) {
39545
+ const page = pages[pageIndex + offset];
39546
+ if (!page) continue;
39547
+ const start = offset === 0 ? lineIndex + 1 : 0;
39548
+ for (let index = start; index < (page.lines?.length ?? 0); index += 1) {
39549
+ const line = page.lines[index];
39550
+ if (!frequencyListItemLine(line)) {
39551
+ if (items.length) return items;
39552
+ continue;
39553
+ }
39554
+ items.push({ text: line, page: page.page });
39555
+ if (items.length >= 10) return items;
39556
+ }
39557
+ }
39558
+ return items;
39559
+ }
39560
+ function betterFrequencyListSupport(best, { pages, pageIndex, lineIndex, answer, answerPhrases, answerTokens, specificTokens, target }) {
39561
+ const page = pages[pageIndex];
39562
+ const heading = page.lines?.[lineIndex] ?? "";
39563
+ const headingNorm = normalizeForSearch(heading);
39564
+ if (frequencyPolarity(headingNorm) !== target) return best;
39565
+ const headingTokens = tokenizeNormalized(headingNorm);
39566
+ const headingFocusHits = tokenHitCount(specificTokens, headingTokens);
39567
+ if (specificTokens.length >= 2 && headingFocusHits <= 0) return best;
39568
+ for (const item of frequencyPolarityListItems(pages, pageIndex, lineIndex)) {
39569
+ if (!containsPhraseOutsideParentheses(item.text, answerPhrases)) continue;
39570
+ const itemTokens = tokenize(item.text);
39571
+ const answerCoverage = strictSoftCoverage(answerTokens, itemTokens);
39572
+ const score = 15.8 + answerCoverage * 4.4 + Math.min(2, headingFocusHits) * 1.2;
39573
+ best = betterEvidence(best, {
39574
+ answerId: answer.id,
39575
+ page: item.page,
39576
+ text: `${heading} ${item.text}`,
39577
+ score,
39578
+ kind: "frequency_polarity_list_item"
39579
+ });
39580
+ }
39581
+ return best;
39582
+ }
39583
+ function bestFrequencyPolaritySupport({ mode, pages, topQuestionPages, question, answer, answerTokens, focusTokens }) {
39584
+ if (mode !== "single") return null;
39585
+ const target = frequencyPolarity(normalizeForSearch(question));
39586
+ if (!target) return null;
39587
+ const answerPhrases = answerSearchPhrases(answer.text).slice(0, 16);
39588
+ const specificTokens = frequencyPolarityFocusTokens(focusTokens, answerTokens);
39589
+ let best = null;
39590
+ for (let pageIndex = 0; pageIndex < pages.length; pageIndex += 1) {
39591
+ const page = pages[pageIndex];
39592
+ if (topQuestionPages?.size && !topQuestionPages.has(page.page) && !topQuestionPages.has(page.page - 1) && !topQuestionPages.has(page.page + 1)) {
39593
+ continue;
39594
+ }
39595
+ for (let lineIndex = 0; lineIndex < (page.lines?.length ?? 0); lineIndex += 1) {
39596
+ best = betterFrequencyListSupport(best, { pages, pageIndex, lineIndex, answer, answerPhrases, answerTokens, specificTokens, target });
39597
+ }
39598
+ for (const segment of cachedLineWindowSegments(page)) {
39599
+ for (const fragment of frequencyPolarityFragments(segment.text)) {
39600
+ const normalized = normalizeForSearch(fragment);
39601
+ if (frequencyPolarity(normalized) !== target) continue;
39602
+ const phraseHit = containsPhraseOutsideParentheses(fragment, answerPhrases);
39603
+ if (!phraseHit) continue;
39604
+ const fragmentTokens = tokenizeNormalized(normalized);
39605
+ const answerCoverage = strictSoftCoverage(answerTokens, fragmentTokens);
39606
+ const focusHits = tokenHitCount(specificTokens, fragmentTokens);
39607
+ if (specificTokens.length >= 2 && focusHits <= 0) continue;
39608
+ const score = 12.2 + (phraseHit ? 2.8 : 0) + answerCoverage * 4.2 + Math.min(2, focusHits) * 1.1;
39609
+ best = betterEvidence(best, {
39610
+ answerId: answer.id,
39611
+ page: page.page,
39612
+ text: fragment,
39613
+ score,
39614
+ kind: "frequency_polarity_segment"
39615
+ });
39616
+ }
39617
+ }
39618
+ }
39619
+ return best;
39620
+ }
38966
39621
  function negatedAnswerPrefixAdjustment({ mode, pages, question, answer, answerTokens }) {
38967
39622
  if (mode !== "single" || answerTokens.length < 2) return { adjustment: 0, evidence: null };
38968
39623
  const questionNorm = normalizeForSearch(question);
@@ -40002,6 +40657,7 @@ ${nextPage.text}`;
40002
40657
  const chunk = bestChunkSupport(context);
40003
40658
  const polarity = polarityAdjustment(context);
40004
40659
  const temporal = temporalCueAdjustment(context);
40660
+ const clinicalCourseCue2 = clinicalCourseCueAdjustment(context);
40005
40661
  const conditionPair = conditionPairAdjustment(context);
40006
40662
  const riskCondition = riskConditionAdjustment(context);
40007
40663
  const genericPopulation = genericPopulationConditionAdjustmentForMode(context);
@@ -40019,10 +40675,13 @@ ${nextPage.text}`;
40019
40675
  const ageEligibility = ageEligibilityAdjustment(context);
40020
40676
  const drugDose = bestDrugDoseSupport(context);
40021
40677
  const termDefinition = bestTermDefinitionSupport(context);
40678
+ const definitionExactAnswer = bestDefinitionExactAnswerSupport(context);
40679
+ const frequencyPolarity2 = bestFrequencyPolaritySupport(context);
40022
40680
  const negatedAnswerPrefix = negatedAnswerPrefixAdjustment(context);
40023
40681
  const impossibilityOnly = impossibilityOnlyAdjustment(context);
40024
40682
  const activeTherapyIndication = activeTherapyIndicationAdjustment(context);
40025
40683
  const recommendationItem = bestRecommendationItemSupport(context);
40684
+ const recommendationBlock = bestRecommendationBlockSupport(context);
40026
40685
  const explicitRecommendationTarget = explicitRecommendationTargetAdjustment(context);
40027
40686
  const conditionedNumber = bestConditionedNumberSupport(context);
40028
40687
  const numericCondition = bestNumericConditionSupport(context);
@@ -40056,7 +40715,7 @@ ${nextPage.text}`;
40056
40715
  const focusedWeight = context.mode === "multi" ? 0.15 : 0.9;
40057
40716
  const lineTokenWeight = context.mode === "single" ? 0.85 : 0;
40058
40717
  const latinFuzzyWeight = context.mode === "multi" && polarity.evidence?.kind !== "polarity_mismatch" ? 1.15 : 0;
40059
- let raw = (anchor?.score ?? 0) * 1.35 + (section?.score ?? 0) * 1.2 + (rowLabel?.score ?? 0) * 0.95 + (focused?.score ?? 0) * focusedWeight + (lineToken?.score ?? 0) * lineTokenWeight + (prefix?.score ?? 0) * 1.15 + (phrase?.score ?? 0) * phraseWeight + (precedingLabel?.score ?? 0) * 1.3 + (exactAnswer?.score ?? 0) * 1.08 + (chunk?.score ?? 0) * 1 + polarity.adjustment + (temporal.support?.score ?? 0) * 1 + temporal.adjustment + conditionPair.adjustment + riskCondition.adjustment + genericPopulation.adjustment + (classSubject?.score ?? 0) * 1.15 + (frequency?.score ?? 0) * 1.1 + negativeLocal.adjustment + (boundedList.support?.score ?? 0) * 1.15 + boundedList.adjustment + (ordinalList?.score ?? 0) * 1.15 + (typeOrdinal?.score ?? 0) * 1.15 + (indicationLabel?.score ?? 0) * 1.15 + (labelDefinition?.score ?? 0) * 1.15 + (recommendationPolarity.support?.score ?? 0) * 1.05 + recommendationPolarity.adjustment + (exactNumericOption?.score ?? 0) * 1.04 + (exactHourAlias?.score ?? 0) * 1.08 + ageEligibility.adjustment + (drugDose?.score ?? 0) * 1.15 + (termDefinition?.score ?? 0) * 1.15 + negatedAnswerPrefix.adjustment + impossibilityOnly.adjustment + activeTherapyIndication.adjustment + (recommendationItem?.score ?? 0) * 1.1 + (explicitRecommendationTarget.support?.score ?? 0) * 1.05 + explicitRecommendationTarget.adjustment + (conditionedNumber?.score ?? 0) * 1.1 + (numericCondition?.score ?? 0) * 1.05 + (countRelation?.score ?? 0) * 1.1 + (ageForm?.score ?? 0) * 1.15 + (fibrosisStage?.score ?? 0) * 1.15 + (conditionNumber?.score ?? 0) * 1.15 + (romanStage?.score ?? 0) * 1.15 + (answerOrdinalRow?.score ?? 0) * 1.15 + (clozeGap?.score ?? 0) * 1.12 + (visualTableColumn?.score ?? 0) * 1.18 + (coordinateTableRow?.score ?? 0) * 1.12 + (coordinateTableGroup?.score ?? 0) * 1.16 + (coordinateMultiCellRow?.score ?? 0) * 1.16 + (coordinateTableMembership?.score ?? 0) * 1.1 + (parentheticalGroup?.score ?? 0) * 1.16 + (questionContinuationList?.score ?? 0) * 1.1 + (shortMedicalAlias?.score ?? 0) * 0.35 + (latinFuzzy?.score ?? 0) * latinFuzzyWeight + (geneSentence?.score ?? 0) * 1.18 + (clinicalFeature.support?.score ?? 0) * 1.12 + clinicalFeature.adjustment + (mkbClassExclusion.support?.score ?? 0) * 1.12 + mkbClassExclusion.adjustment + (labelNumber?.score ?? 0) * 1.15 + (classificationCode?.score ?? 0) * 1.15 + (exactShortLabelRow?.score ?? 0) * 1.2 + (shortLabelRow?.score ?? 0) * 1.15 + (answerPhraseFound ? 0.35 : 0) + (numbers.length ? numberSpecificity(context.answer.text) * 0.35 : 0) + Math.min(0.35, answerTokens.length * 0.015);
40718
+ let raw = (anchor?.score ?? 0) * 1.35 + (section?.score ?? 0) * 1.2 + (rowLabel?.score ?? 0) * 0.95 + (focused?.score ?? 0) * focusedWeight + (lineToken?.score ?? 0) * lineTokenWeight + (prefix?.score ?? 0) * 1.15 + (phrase?.score ?? 0) * phraseWeight + (precedingLabel?.score ?? 0) * 1.3 + (exactAnswer?.score ?? 0) * 1.08 + (chunk?.score ?? 0) * 1 + polarity.adjustment + (temporal.support?.score ?? 0) * 1 + temporal.adjustment + (clinicalCourseCue2.support?.score ?? 0) * 1.05 + clinicalCourseCue2.adjustment + conditionPair.adjustment + riskCondition.adjustment + genericPopulation.adjustment + (classSubject?.score ?? 0) * 1.15 + (frequency?.score ?? 0) * 1.1 + negativeLocal.adjustment + (boundedList.support?.score ?? 0) * 1.15 + boundedList.adjustment + (ordinalList?.score ?? 0) * 1.15 + (typeOrdinal?.score ?? 0) * 1.15 + (indicationLabel?.score ?? 0) * 1.15 + (labelDefinition?.score ?? 0) * 1.15 + (recommendationPolarity.support?.score ?? 0) * 1.05 + recommendationPolarity.adjustment + (exactNumericOption?.score ?? 0) * 1.04 + (exactHourAlias?.score ?? 0) * 1.08 + ageEligibility.adjustment + (drugDose?.score ?? 0) * 1.15 + (termDefinition?.score ?? 0) * 1.15 + (definitionExactAnswer?.score ?? 0) * 1.12 + (frequencyPolarity2?.score ?? 0) * 1.08 + negatedAnswerPrefix.adjustment + impossibilityOnly.adjustment + activeTherapyIndication.adjustment + (recommendationItem?.score ?? 0) * 1.1 + (recommendationBlock?.score ?? 0) * 0.92 + (explicitRecommendationTarget.support?.score ?? 0) * 1.05 + explicitRecommendationTarget.adjustment + (conditionedNumber?.score ?? 0) * 1.1 + (numericCondition?.score ?? 0) * 1.05 + (countRelation?.score ?? 0) * 1.1 + (ageForm?.score ?? 0) * 1.15 + (fibrosisStage?.score ?? 0) * 1.15 + (conditionNumber?.score ?? 0) * 1.15 + (romanStage?.score ?? 0) * 1.15 + (answerOrdinalRow?.score ?? 0) * 1.15 + (clozeGap?.score ?? 0) * 1.12 + (visualTableColumn?.score ?? 0) * 1.18 + (coordinateTableRow?.score ?? 0) * 1.12 + (coordinateTableGroup?.score ?? 0) * 1.16 + (coordinateMultiCellRow?.score ?? 0) * 1.16 + (coordinateTableMembership?.score ?? 0) * 1.1 + (parentheticalGroup?.score ?? 0) * 1.16 + (questionContinuationList?.score ?? 0) * 1.1 + (shortMedicalAlias?.score ?? 0) * 0.35 + (latinFuzzy?.score ?? 0) * latinFuzzyWeight + (geneSentence?.score ?? 0) * 1.18 + (clinicalFeature.support?.score ?? 0) * 1.12 + clinicalFeature.adjustment + (mkbClassExclusion.support?.score ?? 0) * 1.12 + mkbClassExclusion.adjustment + (labelNumber?.score ?? 0) * 1.15 + (classificationCode?.score ?? 0) * 1.15 + (exactShortLabelRow?.score ?? 0) * 1.2 + (shortLabelRow?.score ?? 0) * 1.15 + (answerPhraseFound ? 0.35 : 0) + (numbers.length ? numberSpecificity(context.answer.text) * 0.35 : 0) + Math.min(0.35, answerTokens.length * 0.015);
40060
40719
  if (context.intent.listLike && context.anchorSegments?.length && !anchor) {
40061
40720
  raw *= 0.62;
40062
40721
  }
@@ -40077,6 +40736,8 @@ ${nextPage.text}`;
40077
40736
  polarity.evidence,
40078
40737
  temporal.support,
40079
40738
  temporal.evidence,
40739
+ clinicalCourseCue2.support,
40740
+ clinicalCourseCue2.evidence,
40080
40741
  conditionPair.evidence,
40081
40742
  riskCondition.evidence,
40082
40743
  genericPopulation.evidence,
@@ -40096,10 +40757,13 @@ ${nextPage.text}`;
40096
40757
  ageEligibility.evidence,
40097
40758
  drugDose,
40098
40759
  termDefinition,
40760
+ definitionExactAnswer,
40761
+ frequencyPolarity2,
40099
40762
  negatedAnswerPrefix.evidence,
40100
40763
  impossibilityOnly.evidence,
40101
40764
  activeTherapyIndication.evidence,
40102
40765
  recommendationItem,
40766
+ recommendationBlock,
40103
40767
  explicitRecommendationTarget.support,
40104
40768
  explicitRecommendationTarget.evidence,
40105
40769
  conditionedNumber,
@@ -40130,9 +40794,22 @@ ${nextPage.text}`;
40130
40794
  exactShortLabelRow,
40131
40795
  shortLabelRow
40132
40796
  ].filter(Boolean);
40797
+ const definitionCompletion = definitionCompletionAdjustment(context, evidence);
40798
+ raw += definitionCompletion.adjustment;
40799
+ if (definitionCompletion.evidence) evidence.push(definitionCompletion.evidence);
40133
40800
  const contrastCue = contrastCueMismatchAdjustment(context, evidence.sort((a, b) => b.score - a.score));
40134
40801
  raw += contrastCue.adjustment;
40135
40802
  if (contrastCue.evidence) evidence.push(contrastCue.evidence);
40803
+ if (context.config?.optionFamilyComparatorGuard) {
40804
+ const optionFamilyComparator = optionFamilyComparatorAdjustment({ answer: context.answer, answers: context.answers, evidence });
40805
+ raw += optionFamilyComparator.adjustment;
40806
+ if (optionFamilyComparator.evidence) evidence.push(optionFamilyComparator.evidence);
40807
+ }
40808
+ if (context.config?.optionFamilyCompactComboGuard) {
40809
+ const optionFamilyCompactCombo = optionFamilyCompactComboAdjustment({ question: context.question, answer: context.answer, evidence });
40810
+ raw += optionFamilyCompactCombo.adjustment;
40811
+ if (optionFamilyCompactCombo.evidence) evidence.push(optionFamilyCompactCombo.evidence);
40812
+ }
40136
40813
  const excludedCondition = excludedConditionMismatchAdjustment(context, evidence.sort((a, b) => b.score - a.score));
40137
40814
  raw += excludedCondition.adjustment;
40138
40815
  if (excludedCondition.evidence) evidence.push(excludedCondition.evidence);
@@ -40237,41 +40914,6 @@ ${nextPage.text}`;
40237
40914
  }
40238
40915
  };
40239
40916
  }
40240
- var CONFIDENCE_STRUCTURAL_KINDS = /* @__PURE__ */ new Set([
40241
- "coordinate_table_row",
40242
- "coordinate_table_group",
40243
- "coordinate_table_group_inverse",
40244
- "coordinate_table_multicell_row",
40245
- "coordinate_table_membership",
40246
- "parenthetical_group_segment",
40247
- "preceding_question_label",
40248
- "question_continuation_list",
40249
- "exact_numeric_option_segment",
40250
- "exact_hour_alias_segment",
40251
- "visual_table_column",
40252
- "exact_short_label_visual_row",
40253
- "short_label_visual_row",
40254
- "answer_ordinal_row",
40255
- "fibrosis_stage_row",
40256
- "gene_sentence_segment",
40257
- "clinical_feature_segment",
40258
- "mkb_class_exclusion_absent",
40259
- "classification_code_segment",
40260
- "label_number_proximity",
40261
- "label_definition_segment",
40262
- "row_label_segment",
40263
- "bounded_list_segment",
40264
- "ordinal_list_segment",
40265
- "drug_dose_segment",
40266
- "recommendation_item_segment",
40267
- "explicit_recommendation_target_segment",
40268
- "numeric_condition_less_than",
40269
- "numeric_condition_more_than",
40270
- "numeric_condition_equal",
40271
- "conditioned_number_segment",
40272
- "cloze_gap_local"
40273
- ]);
40274
- var CONFIDENCE_BROAD_KINDS = /* @__PURE__ */ new Set(["bm25_question_answer", "question_chunk_answer", "answer_chunk_question", "answer_window", "focused_answer_window", "shared_multi_segment"]);
40275
40917
  function confidenceEvidenceSummary(item) {
40276
40918
  let bestScore = 0;
40277
40919
  let bestKind = "";
@@ -40283,8 +40925,8 @@ ${nextPage.text}`;
40283
40925
  bestScore = score;
40284
40926
  bestKind = String(evidence.kind ?? "");
40285
40927
  }
40286
- if (CONFIDENCE_STRUCTURAL_KINDS.has(evidence.kind)) structuralScore = Math.max(structuralScore, score);
40287
- if (CONFIDENCE_BROAD_KINDS.has(evidence.kind)) broadCount += 1;
40928
+ if (CONFIDENCE_STRUCTURAL_EVIDENCE_KINDS.has(evidence.kind)) structuralScore = Math.max(structuralScore, score);
40929
+ if (BROAD_EVIDENCE_KINDS.has(evidence.kind)) broadCount += 1;
40288
40930
  }
40289
40931
  return {
40290
40932
  bestScore,