med-pdf-nmo 0.1.0 → 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.
@@ -32079,6 +32079,10 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
32079
32079
  );
32080
32080
  }
32081
32081
  }
32082
+ function pdfVerbosity(pdfjs, options = {}) {
32083
+ if (typeof options.pdfVerbosity === "number") return options.pdfVerbosity;
32084
+ return pdfjs.VerbosityLevel?.ERRORS ?? 0;
32085
+ }
32082
32086
  async function toUint8Array(input) {
32083
32087
  if (input instanceof Uint8Array) {
32084
32088
  return new Uint8Array(input.buffer.slice(input.byteOffset, input.byteOffset + input.byteLength));
@@ -32277,7 +32281,8 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
32277
32281
  data,
32278
32282
  disableWorker: true,
32279
32283
  useSystemFonts: true,
32280
- isEvalSupported: false
32284
+ isEvalSupported: false,
32285
+ verbosity: pdfVerbosity(pdfjs, options)
32281
32286
  });
32282
32287
  const pdf = await loadingTask.promise;
32283
32288
  const pages = [];
@@ -32396,6 +32401,8 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
32396
32401
  singleTieSpecificityGap: 0.5,
32397
32402
  sharedMultiSegmentBoost: true,
32398
32403
  countRelationBoost: true,
32404
+ optionFamilyComparatorGuard: true,
32405
+ optionFamilyCompactComboGuard: true,
32399
32406
  topQuestionChunks: 28,
32400
32407
  evidenceLimit: 8
32401
32408
  };
@@ -32576,6 +32583,100 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
32576
32583
  keyedRuntimeCache.clear();
32577
32584
  }
32578
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
+
32579
32680
  // src/predictor/text-utils.ts
32580
32681
  function nearestCueName(local, entries) {
32581
32682
  const center = Math.floor(local.length / 2);
@@ -32991,6 +33092,11 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
32991
33092
  "\u0434\u043E\u0437\u0430",
32992
33093
  "\u0434\u043E\u0437\u044B",
32993
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",
32994
33100
  "\u043F\u0440\u0438",
32995
33101
  "\u043B\u0435\u0447\u0435\u043D\u0438\u0438",
32996
33102
  "\u043B\u0435\u0447\u0435\u043D\u0438\u044F",
@@ -33054,7 +33160,7 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
33054
33160
  }
33055
33161
  function doseSlashNumbers(sourceText, drugTokens) {
33056
33162
  const out = [];
33057
- const slashPattern = /(\d+(?:[.,]\d+)?)\s*\/\s*(\d+(?:[.,]\d+)?)\s*мг/giu;
33163
+ const slashPattern = /(\d+(?:[.,]\d+)?)\s*\/\s*(\d+(?:[.,]\d+)?)\s*[\u006d\u043c]\u0433/giu;
33058
33164
  for (const match of sourceText.matchAll(slashPattern)) {
33059
33165
  const rawIndex = match.index ?? 0;
33060
33166
  const beforeText = sourceText.slice(Math.max(0, rawIndex - 150), rawIndex);
@@ -33083,7 +33189,7 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
33083
33189
  if (drugIndex < 0) return [];
33084
33190
  const local = normalized.slice(drugIndex, Math.min(normalized.length, drugIndex + 95));
33085
33191
  if (!containsNormalizedPhrase(local, "\u043C\u0433")) return [];
33086
- 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 [];
33087
33193
  const firstNumber = local.match(/\d+(?:[.,]\d+)?/u);
33088
33194
  if (!firstNumber || (firstNumber.index ?? 0) > 55) return [];
33089
33195
  if (local.slice(0, firstNumber.index ?? 0).includes("+")) return [];
@@ -33097,6 +33203,21 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
33097
33203
  if (hasOtherDrugMarker) return [];
33098
33204
  return extractNumbers(local).slice(0, 2).map((number) => String(number).replace(",", "."));
33099
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
+ }
33100
33221
  function normalizeDoseNumber(value) {
33101
33222
  return String(value ?? "").replace(",", ".").replace(/\.0$/u, "");
33102
33223
  }
@@ -33115,12 +33236,22 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
33115
33236
  const normalized = normalizeForSearch(sourceText);
33116
33237
  const drugIndex = drugTokenIndex(normalized, drugTokens);
33117
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
+ }
33118
33243
  const local = normalized.slice(drugIndex, Math.min(normalized.length, drugIndex + 125));
33119
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
+ }
33120
33249
  const dosePattern = /(\d+(?:[.,]\d+)?)(?:\s*-\s*(\d+(?:[.,]\d+)?))?\s*[\u006d\u043c]\u0433(?:\s*[\u0078\u0445]\s*(\d+(?:[.,]\d+)?))?/giu;
33121
33250
  for (const match of local.matchAll(dosePattern)) {
33122
33251
  const index = match.index ?? 0;
33123
33252
  if (index > 80) continue;
33253
+ const beforeNumber = local.slice(0, index).replace(/\s+$/u, "");
33254
+ if (beforeNumber.endsWith("/")) continue;
33124
33255
  facts.push({
33125
33256
  dose: normalizeDoseNumber(match[2] ?? match[1]),
33126
33257
  doseRange: match[2] ? [normalizeDoseNumber(match[1]), normalizeDoseNumber(match[2])] : null,
@@ -33128,7 +33259,7 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
33128
33259
  });
33129
33260
  break;
33130
33261
  }
33131
- for (const number of [...doseSlashNumbers(sourceText, drugTokens), ...doseNearDrugNumbers(sourceText, drugTokens)]) {
33262
+ for (const number of doseNearDrugNumbers(sourceText, drugTokens)) {
33132
33263
  facts.push({ dose: normalizeDoseNumber(number), doseRange: null, frequency: null });
33133
33264
  }
33134
33265
  return facts;
@@ -35267,6 +35398,220 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
35267
35398
  }
35268
35399
  return best;
35269
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
+ }
35270
35615
 
35271
35616
  // src/predictor/scorers/direction.ts
35272
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));
@@ -35378,6 +35723,117 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
35378
35723
  if (bestMismatch && (!bestMatch || bestMismatch.score > bestMatch.score + 0.4)) return { support: null, adjustment: -4.8, evidence: bestMismatch };
35379
35724
  return { support: null, adjustment: 0, evidence: null };
35380
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
+ }
35381
35837
  var CONTRAST_CUE_GROUPS = [
35382
35838
  {
35383
35839
  answer: ["\u0432\u0435\u0440\u0445\u043D"],
@@ -36880,52 +37336,6 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
36880
37336
  }
36881
37337
 
36882
37338
  // src/predictor/selection.ts
36883
- var STRUCTURAL_EVIDENCE_WEIGHTS = new Map(
36884
- Object.entries({
36885
- coordinate_table_row: 1.25,
36886
- coordinate_table_group: 1.25,
36887
- coordinate_table_group_inverse: 1.25,
36888
- coordinate_table_multicell_row: 1.25,
36889
- coordinate_table_membership: 1.15,
36890
- parenthetical_group_segment: 1.05,
36891
- preceding_question_label: 1.05,
36892
- question_continuation_list: 1.05,
36893
- exact_numeric_option_segment: 1.05,
36894
- exact_hour_alias_segment: 1.05,
36895
- short_medical_alias_segment: 0.9,
36896
- visual_table_column: 1.2,
36897
- exact_short_label_visual_row: 1.15,
36898
- short_label_visual_row: 1.05,
36899
- answer_ordinal_row: 1.05,
36900
- fibrosis_stage_row: 1.2,
36901
- gene_sentence_segment: 1.1,
36902
- clinical_feature_segment: 1,
36903
- mkb_class_exclusion_absent: 1,
36904
- classification_code_segment: 1.15,
36905
- label_number_proximity: 1,
36906
- label_definition_segment: 1,
36907
- row_label_segment: 0.95,
36908
- bounded_list_segment: 0.95,
36909
- ordinal_list_segment: 0.9,
36910
- drug_dose_segment: 0.9,
36911
- recommendation_item_segment: 0.85,
36912
- explicit_recommendation_target_segment: 0.85,
36913
- numeric_condition_less_than: 0.85,
36914
- numeric_condition_more_than: 0.85,
36915
- numeric_condition_equal: 0.85,
36916
- conditioned_number_segment: 0.8,
36917
- cloze_gap_local: 0.8
36918
- })
36919
- );
36920
- var BROAD_EVIDENCE_KINDS = /* @__PURE__ */ new Set([
36921
- "bm25_question_answer",
36922
- "question_chunk_answer",
36923
- "answer_chunk_question",
36924
- "answer_window",
36925
- "focused_answer_window",
36926
- "shared_multi_segment"
36927
- ]);
36928
- var NOISY_SHARED_EVIDENCE_KINDS = /* @__PURE__ */ new Set(["question_chunk_answer", "bm25_question_answer", "shared_multi_segment"]);
36929
37339
  function calibrateScores(answerScores) {
36930
37340
  const rawValues = answerScores.map((item) => item.raw);
36931
37341
  const max = Math.max(...rawValues, 1e-4);
@@ -37026,7 +37436,7 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4<f32> {
37026
37436
  bestScore = score;
37027
37437
  bestKind = evidence.kind;
37028
37438
  }
37029
- const weight = STRUCTURAL_EVIDENCE_WEIGHTS.get(evidence.kind) ?? 0;
37439
+ const weight = SELECTION_STRUCTURAL_EVIDENCE_WEIGHTS.get(evidence.kind) ?? 0;
37030
37440
  if (weight > 0) {
37031
37441
  bestStructuralScore = Math.max(bestStructuralScore, score);
37032
37442
  structuralWeight = Math.max(structuralWeight, weight);
@@ -38958,6 +39368,256 @@ ${nextPage.text}`;
38958
39368
  }
38959
39369
  return best;
38960
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
+ }
38961
39621
  function negatedAnswerPrefixAdjustment({ mode, pages, question, answer, answerTokens }) {
38962
39622
  if (mode !== "single" || answerTokens.length < 2) return { adjustment: 0, evidence: null };
38963
39623
  const questionNorm = normalizeForSearch(question);
@@ -39997,6 +40657,7 @@ ${nextPage.text}`;
39997
40657
  const chunk = bestChunkSupport(context);
39998
40658
  const polarity = polarityAdjustment(context);
39999
40659
  const temporal = temporalCueAdjustment(context);
40660
+ const clinicalCourseCue2 = clinicalCourseCueAdjustment(context);
40000
40661
  const conditionPair = conditionPairAdjustment(context);
40001
40662
  const riskCondition = riskConditionAdjustment(context);
40002
40663
  const genericPopulation = genericPopulationConditionAdjustmentForMode(context);
@@ -40014,10 +40675,13 @@ ${nextPage.text}`;
40014
40675
  const ageEligibility = ageEligibilityAdjustment(context);
40015
40676
  const drugDose = bestDrugDoseSupport(context);
40016
40677
  const termDefinition = bestTermDefinitionSupport(context);
40678
+ const definitionExactAnswer = bestDefinitionExactAnswerSupport(context);
40679
+ const frequencyPolarity2 = bestFrequencyPolaritySupport(context);
40017
40680
  const negatedAnswerPrefix = negatedAnswerPrefixAdjustment(context);
40018
40681
  const impossibilityOnly = impossibilityOnlyAdjustment(context);
40019
40682
  const activeTherapyIndication = activeTherapyIndicationAdjustment(context);
40020
40683
  const recommendationItem = bestRecommendationItemSupport(context);
40684
+ const recommendationBlock = bestRecommendationBlockSupport(context);
40021
40685
  const explicitRecommendationTarget = explicitRecommendationTargetAdjustment(context);
40022
40686
  const conditionedNumber = bestConditionedNumberSupport(context);
40023
40687
  const numericCondition = bestNumericConditionSupport(context);
@@ -40051,7 +40715,7 @@ ${nextPage.text}`;
40051
40715
  const focusedWeight = context.mode === "multi" ? 0.15 : 0.9;
40052
40716
  const lineTokenWeight = context.mode === "single" ? 0.85 : 0;
40053
40717
  const latinFuzzyWeight = context.mode === "multi" && polarity.evidence?.kind !== "polarity_mismatch" ? 1.15 : 0;
40054
- 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);
40055
40719
  if (context.intent.listLike && context.anchorSegments?.length && !anchor) {
40056
40720
  raw *= 0.62;
40057
40721
  }
@@ -40072,6 +40736,8 @@ ${nextPage.text}`;
40072
40736
  polarity.evidence,
40073
40737
  temporal.support,
40074
40738
  temporal.evidence,
40739
+ clinicalCourseCue2.support,
40740
+ clinicalCourseCue2.evidence,
40075
40741
  conditionPair.evidence,
40076
40742
  riskCondition.evidence,
40077
40743
  genericPopulation.evidence,
@@ -40091,10 +40757,13 @@ ${nextPage.text}`;
40091
40757
  ageEligibility.evidence,
40092
40758
  drugDose,
40093
40759
  termDefinition,
40760
+ definitionExactAnswer,
40761
+ frequencyPolarity2,
40094
40762
  negatedAnswerPrefix.evidence,
40095
40763
  impossibilityOnly.evidence,
40096
40764
  activeTherapyIndication.evidence,
40097
40765
  recommendationItem,
40766
+ recommendationBlock,
40098
40767
  explicitRecommendationTarget.support,
40099
40768
  explicitRecommendationTarget.evidence,
40100
40769
  conditionedNumber,
@@ -40125,9 +40794,22 @@ ${nextPage.text}`;
40125
40794
  exactShortLabelRow,
40126
40795
  shortLabelRow
40127
40796
  ].filter(Boolean);
40797
+ const definitionCompletion = definitionCompletionAdjustment(context, evidence);
40798
+ raw += definitionCompletion.adjustment;
40799
+ if (definitionCompletion.evidence) evidence.push(definitionCompletion.evidence);
40128
40800
  const contrastCue = contrastCueMismatchAdjustment(context, evidence.sort((a, b) => b.score - a.score));
40129
40801
  raw += contrastCue.adjustment;
40130
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
+ }
40131
40813
  const excludedCondition = excludedConditionMismatchAdjustment(context, evidence.sort((a, b) => b.score - a.score));
40132
40814
  raw += excludedCondition.adjustment;
40133
40815
  if (excludedCondition.evidence) evidence.push(excludedCondition.evidence);
@@ -40143,7 +40825,8 @@ ${nextPage.text}`;
40143
40825
  if (!answers.length) throw new Error("predict input requires answers");
40144
40826
  const runtime = await getPdfRuntime(pdfInput, {
40145
40827
  cacheKey: input.cacheKey ?? input.pdfPath ?? input.pdfUrl ?? input.url,
40146
- pdfjsLib: options.pdfjsLib
40828
+ pdfjsLib: options.pdfjsLib,
40829
+ pdfVerbosity: options.pdfVerbosity
40147
40830
  });
40148
40831
  const question = String(input.question ?? "");
40149
40832
  const questionTokens = uniqueTokens(question);
@@ -40231,41 +40914,6 @@ ${nextPage.text}`;
40231
40914
  }
40232
40915
  };
40233
40916
  }
40234
- var CONFIDENCE_STRUCTURAL_KINDS = /* @__PURE__ */ new Set([
40235
- "coordinate_table_row",
40236
- "coordinate_table_group",
40237
- "coordinate_table_group_inverse",
40238
- "coordinate_table_multicell_row",
40239
- "coordinate_table_membership",
40240
- "parenthetical_group_segment",
40241
- "preceding_question_label",
40242
- "question_continuation_list",
40243
- "exact_numeric_option_segment",
40244
- "exact_hour_alias_segment",
40245
- "visual_table_column",
40246
- "exact_short_label_visual_row",
40247
- "short_label_visual_row",
40248
- "answer_ordinal_row",
40249
- "fibrosis_stage_row",
40250
- "gene_sentence_segment",
40251
- "clinical_feature_segment",
40252
- "mkb_class_exclusion_absent",
40253
- "classification_code_segment",
40254
- "label_number_proximity",
40255
- "label_definition_segment",
40256
- "row_label_segment",
40257
- "bounded_list_segment",
40258
- "ordinal_list_segment",
40259
- "drug_dose_segment",
40260
- "recommendation_item_segment",
40261
- "explicit_recommendation_target_segment",
40262
- "numeric_condition_less_than",
40263
- "numeric_condition_more_than",
40264
- "numeric_condition_equal",
40265
- "conditioned_number_segment",
40266
- "cloze_gap_local"
40267
- ]);
40268
- var CONFIDENCE_BROAD_KINDS = /* @__PURE__ */ new Set(["bm25_question_answer", "question_chunk_answer", "answer_chunk_question", "answer_window", "focused_answer_window", "shared_multi_segment"]);
40269
40917
  function confidenceEvidenceSummary(item) {
40270
40918
  let bestScore = 0;
40271
40919
  let bestKind = "";
@@ -40277,8 +40925,8 @@ ${nextPage.text}`;
40277
40925
  bestScore = score;
40278
40926
  bestKind = String(evidence.kind ?? "");
40279
40927
  }
40280
- if (CONFIDENCE_STRUCTURAL_KINDS.has(evidence.kind)) structuralScore = Math.max(structuralScore, score);
40281
- 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;
40282
40930
  }
40283
40931
  return {
40284
40932
  bestScore,
@@ -40387,7 +41035,7 @@ ${nextPage.text}`;
40387
41035
  answers,
40388
41036
  mode: options.type ?? options.mode ?? "single"
40389
41037
  },
40390
- { pdfjsLib: options.pdfjsLib }
41038
+ { pdfjsLib: options.pdfjsLib, pdfVerbosity: options.pdfVerbosity }
40391
41039
  );
40392
41040
  const selectedAnswers = output.selected.map((id) => answers.find((answer) => answer.id === id)).filter(Boolean);
40393
41041
  return {