scientify 1.12.2 → 1.13.1

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.
@@ -16,6 +16,12 @@ const MAX_STRICT_FULLTEXT_ATTEMPTS = 5;
16
16
  const ARXIV_API_URL = "https://export.arxiv.org/api/query";
17
17
  const STRICT_EMPTY_FALLBACK_MAX_RESULTS = 12;
18
18
  const STRICT_EMPTY_FALLBACK_MAX_QUERIES = 4;
19
+ const DEFAULT_STRICT_CANDIDATE_POOL = 24;
20
+ const DEFAULT_STRICT_MIN_CORE_FLOOR = 3;
21
+ const TIER_A_RATIO = 0.5;
22
+ const TIER_B_RATIO = 0.35;
23
+ const TIER_C_RATIO = 0.15;
24
+ const REFLECTION_MAX_ADDED_PAPERS = 2;
19
25
  const FEEDBACK_SIGNAL_DELTA = {
20
26
  read: 1,
21
27
  skip: -1,
@@ -390,6 +396,98 @@ function tokenizeKeywords(raw) {
390
396
  }
391
397
  return [...seen];
392
398
  }
399
+ function inferTopicAliases(tokens) {
400
+ const normalized = tokens
401
+ .map((token) => token.toLowerCase())
402
+ .filter((token) => /^[a-z][a-z0-9_-]*$/.test(token))
403
+ .slice(0, 6);
404
+ if (normalized.length < 3)
405
+ return [];
406
+ const aliases = new Set();
407
+ const [a, b, c] = normalized;
408
+ if (a.length >= 2 && b.length >= 1 && c.length >= 1) {
409
+ aliases.add(`${a.slice(0, 2)}${b[0]}${c[0]}`);
410
+ }
411
+ aliases.add(`${a[0]}${b[0]}${c[0]}`);
412
+ const hasLow = normalized.includes("low");
413
+ const hasRank = normalized.includes("rank");
414
+ const hasAdapt = normalized.some((token) => token.startsWith("adapt"));
415
+ if (hasLow && hasRank && hasAdapt)
416
+ aliases.add("lora");
417
+ return [...aliases].filter((alias) => alias.length >= 3 && alias.length <= 8);
418
+ }
419
+ function buildScoringTokens(topic) {
420
+ const stopwords = new Set([
421
+ "from",
422
+ "with",
423
+ "without",
424
+ "first",
425
+ "basics",
426
+ "basic",
427
+ "foundational",
428
+ "foundation",
429
+ "seminal",
430
+ "classic",
431
+ "avoid",
432
+ "benchmark",
433
+ "only",
434
+ "prefer",
435
+ "authoritative",
436
+ "latest",
437
+ "recent",
438
+ "paper",
439
+ "papers",
440
+ "study",
441
+ "works",
442
+ ]);
443
+ const rawTokens = tokenizeKeywords(topic);
444
+ const aliases = inferTopicAliases(rawTokens);
445
+ const base = rawTokens.filter((token) => token.length >= 4 && !stopwords.has(token));
446
+ if (base.length > 0)
447
+ return [...new Set([...base, ...aliases])].slice(0, 10);
448
+ return [...new Set([...rawTokens, ...aliases])].slice(0, 10);
449
+ }
450
+ function buildRetrievalSeedTokens(topic) {
451
+ const directiveWords = new Set([
452
+ "from",
453
+ "with",
454
+ "without",
455
+ "first",
456
+ "basics",
457
+ "basic",
458
+ "foundational",
459
+ "foundation",
460
+ "seminal",
461
+ "classic",
462
+ "avoid",
463
+ "benchmark",
464
+ "only",
465
+ "prefer",
466
+ "authoritative",
467
+ "latest",
468
+ "recent",
469
+ "paper",
470
+ "papers",
471
+ "study",
472
+ "works",
473
+ "strict",
474
+ "fast",
475
+ ]);
476
+ const rawTokens = tokenizeKeywords(topic);
477
+ const aliases = inferTopicAliases(rawTokens);
478
+ const tokens = rawTokens
479
+ .map((token) => token.toLowerCase())
480
+ .filter((token) => token.length >= 3 && !directiveWords.has(token));
481
+ return [...new Set([...tokens, ...aliases])].slice(0, 10);
482
+ }
483
+ const FOUNDATIONAL_HINT_RE = /\b(foundational|foundation|seminal|classic|groundwork|original paper|from basics|start from basics|first principles)\b|\u57fa\u7840|\u5950\u57fa|\u7ecf\u5178|\u539f\u59cb/u;
484
+ const AVOID_BENCHMARK_HINT_RE = /\b(avoid benchmark|benchmark-only|no benchmark|less benchmark|not benchmark only)\b|\u5c11\u63a8.*benchmark|\u4e0d\u8981.*benchmark/u;
485
+ const SURVEY_HINT_RE = /\b(survey|review|taxonomy|overview|tutorial)\b|\u7efc\u8ff0|\u8bc4\u8ff0/u;
486
+ const AUTHORITY_HINT_RE = /\b(authoritative|high impact|top-tier|highly cited|landmark|canonical)\b|\u6743\u5a01|\u9ad8\u5f15\u7528/u;
487
+ const RECENT_HINT_RE = /\b(latest|recent|state[- ]of[- ]the[- ]art|newest)\b|\u6700\u65b0|\u8fd1\u671f/u;
488
+ const BENCHMARK_WORD_RE = /\b(benchmark|leaderboard|dataset|evaluation)\b/i;
489
+ const METHOD_WORD_RE = /\b(method|approach|adaptation|training|fine[- ]?tuning|optimization|algorithm|framework|model)\b/i;
490
+ const SURVEY_WORD_RE = /\b(survey|review|taxonomy|overview|tutorial)\b/i;
393
491
  function decodeXmlEntities(raw) {
394
492
  return raw
395
493
  .replace(/&lt;/g, "<")
@@ -426,16 +524,7 @@ function parseArxivAtomCandidates(xml) {
426
524
  }
427
525
  return parsed;
428
526
  }
429
- function buildStrictFallbackQueries(topic) {
430
- const normalizedTopic = normalizeText(topic);
431
- const queries = [normalizedTopic];
432
- const tokens = tokenizeKeywords(normalizedTopic).filter((token) => token.length >= 3).slice(0, 8);
433
- if (tokens.length >= 2) {
434
- queries.push(tokens.join(" "));
435
- }
436
- if (tokens.length >= 4) {
437
- queries.push(tokens.slice(0, 4).join(" "));
438
- }
527
+ function dedupeQueries(queries, limit) {
439
528
  const seen = new Set();
440
529
  const deduped = [];
441
530
  for (const query of queries) {
@@ -444,28 +533,139 @@ function buildStrictFallbackQueries(topic) {
444
533
  continue;
445
534
  seen.add(key);
446
535
  deduped.push(query);
536
+ if (deduped.length >= limit)
537
+ break;
447
538
  }
448
- return deduped.slice(0, STRICT_EMPTY_FALLBACK_MAX_QUERIES);
539
+ return deduped;
540
+ }
541
+ function buildStrictFallbackQueries(topic) {
542
+ const seedTokens = buildRetrievalSeedTokens(topic);
543
+ const normalizedTopic = seedTokens.length > 0 ? seedTokens.join(" ") : normalizeText(topic);
544
+ const tokens = seedTokens.length > 0 ? seedTokens : tokenizeKeywords(normalizedTopic).filter((token) => token.length >= 3).slice(0, 10);
545
+ const queries = [normalizedTopic];
546
+ if (tokens.length >= 2)
547
+ queries.push(tokens.slice(0, 4).join(" "));
548
+ if (tokens.length >= 3)
549
+ queries.push(tokens.slice(0, 3).join(" "));
550
+ return dedupeQueries(queries, STRICT_EMPTY_FALLBACK_MAX_QUERIES);
551
+ }
552
+ function buildTieredFallbackQueries(topic) {
553
+ const seedTokens = buildRetrievalSeedTokens(topic);
554
+ const normalizedTopic = seedTokens.length > 0 ? seedTokens.join(" ") : normalizeText(topic);
555
+ const tokens = seedTokens.length > 0 ? seedTokens : tokenizeKeywords(normalizedTopic).filter((token) => token.length >= 3).slice(0, 10);
556
+ const tierA = buildStrictFallbackQueries(topic);
557
+ const tierB = dedupeQueries([
558
+ ...tokens.slice(0, 6).map((token) => `${token} adaptation`),
559
+ ...tokens.slice(0, 6).map((token) => `${token} method`),
560
+ ...tokens.slice(0, 4).map((token) => `${token} framework`),
561
+ tokens.slice(0, 4).join(" "),
562
+ ], STRICT_EMPTY_FALLBACK_MAX_QUERIES);
563
+ const tierC = dedupeQueries([
564
+ ...tokens.slice(0, 5).map((token) => `${token} transfer learning`),
565
+ ...tokens.slice(0, 5).map((token) => `${token} benchmark`),
566
+ ...tokens.slice(0, 5).map((token) => `${token} retrieval`),
567
+ `${normalizedTopic} cross domain`,
568
+ ], STRICT_EMPTY_FALLBACK_MAX_QUERIES);
569
+ return {
570
+ tierA: tierA.length > 0 ? tierA : [normalizedTopic],
571
+ tierB,
572
+ tierC,
573
+ };
574
+ }
575
+ function inferRequirementProfile(raw) {
576
+ const text = normalizeText(raw);
577
+ return {
578
+ foundationalFirst: FOUNDATIONAL_HINT_RE.test(text),
579
+ avoidBenchmarkOnly: AVOID_BENCHMARK_HINT_RE.test(text),
580
+ preferSurvey: SURVEY_HINT_RE.test(text),
581
+ preferAuthority: AUTHORITY_HINT_RE.test(text),
582
+ preferRecent: RECENT_HINT_RE.test(text),
583
+ };
584
+ }
585
+ function inferCandidateYear(paper) {
586
+ if (paper.published) {
587
+ const ts = Date.parse(paper.published);
588
+ if (Number.isFinite(ts))
589
+ return new Date(ts).getUTCFullYear();
590
+ }
591
+ const modern = paper.id.match(/:(\d{2})(\d{2})\./);
592
+ if (modern?.[1]) {
593
+ const yy = Number.parseInt(modern[1], 10);
594
+ if (Number.isFinite(yy))
595
+ return 2000 + yy;
596
+ }
597
+ return undefined;
598
+ }
599
+ function isBenchmarkOnlyPaper(paper) {
600
+ const text = `${paper.title} ${paper.summary ?? ""}`;
601
+ return BENCHMARK_WORD_RE.test(text) && !METHOD_WORD_RE.test(text);
602
+ }
603
+ function isSurveyPaper(paper) {
604
+ const text = `${paper.title} ${paper.summary ?? ""}`;
605
+ return SURVEY_WORD_RE.test(text);
606
+ }
607
+ function isFoundationalPaper(args) {
608
+ const year = args.year;
609
+ const nowYear = new Date().getUTCFullYear();
610
+ const oldEnough = typeof year === "number" ? year <= nowYear - 2 : false;
611
+ const title = normalizeText(args.paper.title).toLowerCase();
612
+ const tokenHit = args.topicTokens.some((token) => token.length >= 4 && title.includes(token));
613
+ return oldEnough || tokenHit;
449
614
  }
450
615
  function countTokenOverlap(tokens, text) {
451
- const hay = ` ${normalizeText(text).toLowerCase()} `;
616
+ const hay = ` ${normalizeText(text)
617
+ .toLowerCase()
618
+ .replace(/[_-]+/g, " ")
619
+ .replace(/[^\p{L}\p{N}\s]+/gu, " ")
620
+ .replace(/\s+/g, " ")} `;
452
621
  let score = 0;
453
622
  for (const token of tokens) {
454
623
  if (token.length < 2)
455
624
  continue;
456
- if (hay.includes(` ${token} `))
625
+ const normalizedToken = token
626
+ .toLowerCase()
627
+ .replace(/[_-]+/g, " ")
628
+ .replace(/[^\p{L}\p{N}\s]+/gu, " ")
629
+ .trim();
630
+ if (!normalizedToken)
631
+ continue;
632
+ if (hay.includes(` ${normalizedToken} `))
457
633
  score += 1;
458
634
  }
459
635
  return score;
460
636
  }
461
- function scoreFallbackCandidate(topicTokens, paper) {
637
+ function scoreFallbackCandidate(topicTokens, paper, tier, requirements) {
462
638
  const titleOverlap = countTokenOverlap(topicTokens, paper.title);
463
639
  const abstractOverlap = countTokenOverlap(topicTokens, paper.summary ?? "");
464
640
  const publishedAt = paper.published ? Date.parse(paper.published) : NaN;
465
641
  const recencyBoost = Number.isFinite(publishedAt)
466
642
  ? Math.max(0, Math.min(8, (Date.now() - publishedAt) / (1000 * 60 * 60 * 24 * -180)))
467
643
  : 0;
468
- const rawScore = 60 + titleOverlap * 8 + abstractOverlap * 3 + recencyBoost;
644
+ const tierBoost = tier === "tierA" ? 8 : tier === "tierB" ? 4 : 1;
645
+ const year = inferCandidateYear(paper);
646
+ const isBenchmarkOnly = isBenchmarkOnlyPaper(paper);
647
+ const isSurvey = isSurveyPaper(paper);
648
+ const isFoundational = isFoundationalPaper({ paper, year, topicTokens });
649
+ const nowYear = new Date().getUTCFullYear();
650
+ const recencyPenalty = typeof year === "number" && year >= nowYear ? 4 : 0;
651
+ let rawScore = 60 + tierBoost + titleOverlap * 8 + abstractOverlap * 3 + recencyBoost - recencyPenalty;
652
+ if (requirements.foundationalFirst) {
653
+ rawScore += isFoundational ? 10 : -4;
654
+ }
655
+ if (requirements.preferSurvey) {
656
+ rawScore += isSurvey ? 8 : 0;
657
+ }
658
+ if (requirements.preferAuthority) {
659
+ rawScore += isSurvey ? 3 : 0;
660
+ if (isFoundational)
661
+ rawScore += 2;
662
+ }
663
+ if (requirements.preferRecent && typeof year === "number" && year >= nowYear - 1) {
664
+ rawScore += 4;
665
+ }
666
+ if (requirements.avoidBenchmarkOnly && isBenchmarkOnly) {
667
+ rawScore -= 15;
668
+ }
469
669
  return Math.max(50, Math.min(99, Math.round(rawScore)));
470
670
  }
471
671
  async function fetchArxivFallbackByQuery(query) {
@@ -498,34 +698,139 @@ async function fetchArxivFallbackByQuery(query) {
498
698
  }
499
699
  }
500
700
  async function strictCoreFallbackSeed(args) {
501
- const queries = buildStrictFallbackQueries(args.topic);
701
+ const tieredQueries = buildTieredFallbackQueries(args.topic);
502
702
  const byId = new Map();
503
703
  const traces = [];
504
- for (const query of queries) {
505
- const rows = await fetchArxivFallbackByQuery(query);
506
- traces.push({
507
- query,
508
- reason: "strict_core_backfill_seed",
509
- source: "arxiv",
510
- candidates: rows.length,
511
- filteredTo: rows.length,
512
- resultCount: rows.length,
513
- });
514
- for (const row of rows) {
515
- if (!byId.has(row.id))
516
- byId.set(row.id, row);
704
+ const tierStats = {
705
+ tierA: { candidates: 0, selected: 0 },
706
+ tierB: { candidates: 0, selected: 0 },
707
+ tierC: { candidates: 0, selected: 0 },
708
+ };
709
+ for (const tier of ["tierA", "tierB", "tierC"]) {
710
+ for (const query of tieredQueries[tier]) {
711
+ const rows = await fetchArxivFallbackByQuery(query);
712
+ tierStats[tier].candidates += rows.length;
713
+ traces.push({
714
+ query,
715
+ reason: `strict_core_backfill_seed_${tier}`,
716
+ source: "arxiv",
717
+ candidates: rows.length,
718
+ filteredTo: rows.length,
719
+ resultCount: rows.length,
720
+ });
721
+ for (const row of rows) {
722
+ if (!byId.has(row.id))
723
+ byId.set(row.id, { row, tier });
724
+ }
517
725
  }
518
726
  }
519
727
  const topicTokens = tokenizeKeywords(args.topic);
728
+ const scoringTokens = buildScoringTokens(args.topic);
520
729
  const ranked = [...byId.values()]
521
- .map((row) => ({
522
- row,
523
- score: scoreFallbackCandidate(topicTokens, row),
524
- }))
730
+ .map(({ row, tier }) => {
731
+ const year = inferCandidateYear(row);
732
+ const isSurvey = isSurveyPaper(row);
733
+ const isBenchmarkOnly = isBenchmarkOnlyPaper(row);
734
+ const isFoundational = isFoundationalPaper({ paper: row, year, topicTokens });
735
+ const relevance = countTokenOverlap(scoringTokens, `${row.title} ${row.summary ?? ""}`);
736
+ return {
737
+ row,
738
+ tier,
739
+ year,
740
+ isSurvey,
741
+ isBenchmarkOnly,
742
+ isFoundational,
743
+ relevance,
744
+ score: scoreFallbackCandidate(scoringTokens.length > 0 ? scoringTokens : topicTokens, row, tier, args.requirements),
745
+ };
746
+ })
525
747
  .sort((a, b) => b.score - a.score);
526
748
  const unseen = ranked.filter((item) => !args.knownPaperIds.has(item.row.id));
527
- const effectivePool = unseen.length > 0 ? unseen : ranked;
528
- const selected = effectivePool.slice(0, Math.max(1, Math.min(10, args.maxPapers)));
749
+ const poolBeforeRelevance = unseen.length > 0 ? unseen : ranked;
750
+ const minRelevance = scoringTokens.length >= 2 ? 2 : 1;
751
+ const candidatePool = Math.max(1, Math.min(40, Math.floor(args.candidatePool ?? Math.max(DEFAULT_STRICT_CANDIDATE_POOL, args.maxPapers * 4))));
752
+ const minCoreFloor = Math.max(1, Math.min(args.maxPapers, args.minCoreFloor ?? DEFAULT_STRICT_MIN_CORE_FLOOR));
753
+ const effectivePoolByRelevance = poolBeforeRelevance.filter((item) => item.relevance >= minRelevance);
754
+ const focusTokens = scoringTokens.filter((token) => token.length >= 5);
755
+ const weakRelevanceWithFocusPool = poolBeforeRelevance.filter((item) => {
756
+ if (item.relevance < 1)
757
+ return false;
758
+ if (focusTokens.length === 0)
759
+ return true;
760
+ const focusHit = countTokenOverlap(focusTokens, `${item.row.title} ${item.row.summary ?? ""}`);
761
+ return focusHit >= 1;
762
+ });
763
+ const weakRelevancePool = weakRelevanceWithFocusPool.length > 0
764
+ ? weakRelevanceWithFocusPool
765
+ : poolBeforeRelevance.filter((item) => item.relevance >= 1);
766
+ const effectivePool = effectivePoolByRelevance.length >= minCoreFloor
767
+ ? effectivePoolByRelevance
768
+ : weakRelevancePool.length > 0
769
+ ? weakRelevancePool
770
+ : poolBeforeRelevance;
771
+ const targetCount = Math.max(minCoreFloor, Math.min(args.maxPapers, candidatePool));
772
+ const tierTargets = {
773
+ tierA: Math.max(1, Math.round(targetCount * TIER_A_RATIO)),
774
+ tierB: Math.max(1, Math.round(targetCount * TIER_B_RATIO)),
775
+ tierC: Math.max(0, targetCount - Math.round(targetCount * TIER_A_RATIO) - Math.round(targetCount * TIER_B_RATIO)),
776
+ };
777
+ if (tierTargets.tierA + tierTargets.tierB + tierTargets.tierC < targetCount) {
778
+ tierTargets.tierA += targetCount - (tierTargets.tierA + tierTargets.tierB + tierTargets.tierC);
779
+ }
780
+ const selected = [];
781
+ const selectedIds = new Set();
782
+ for (const tier of ["tierA", "tierB", "tierC"]) {
783
+ const picked = effectivePool
784
+ .filter((item) => item.tier === tier && !selectedIds.has(item.row.id))
785
+ .slice(0, tierTargets[tier]);
786
+ for (const item of picked) {
787
+ selected.push(item);
788
+ selectedIds.add(item.row.id);
789
+ tierStats[tier].selected += 1;
790
+ }
791
+ }
792
+ if (selected.length < targetCount) {
793
+ const fill = effectivePool.filter((item) => !selectedIds.has(item.row.id)).slice(0, targetCount - selected.length);
794
+ for (const item of fill) {
795
+ selected.push(item);
796
+ selectedIds.add(item.row.id);
797
+ tierStats[item.tier].selected += 1;
798
+ }
799
+ }
800
+ const ensureAtLeast = (predicate, need) => {
801
+ while (selected.filter(predicate).length < need) {
802
+ const candidate = effectivePool.find((item) => !selectedIds.has(item.row.id) && predicate(item));
803
+ if (!candidate)
804
+ break;
805
+ const replaceIndex = selected.findIndex((item) => !predicate(item));
806
+ if (replaceIndex < 0)
807
+ break;
808
+ selectedIds.delete(selected[replaceIndex].row.id);
809
+ selected[replaceIndex] = candidate;
810
+ selectedIds.add(candidate.row.id);
811
+ }
812
+ };
813
+ if (args.requirements.foundationalFirst) {
814
+ ensureAtLeast((item) => item.isFoundational, Math.min(2, targetCount));
815
+ }
816
+ if (args.requirements.preferSurvey) {
817
+ ensureAtLeast((item) => item.isSurvey, 1);
818
+ }
819
+ if (args.requirements.avoidBenchmarkOnly) {
820
+ for (let i = 0; i < selected.length; i += 1) {
821
+ if (!selected[i].isBenchmarkOnly)
822
+ continue;
823
+ const replacement = effectivePool.find((item) => !selectedIds.has(item.row.id) && !item.isBenchmarkOnly);
824
+ if (!replacement)
825
+ break;
826
+ selectedIds.delete(selected[i].row.id);
827
+ selected[i] = replacement;
828
+ selectedIds.add(replacement.row.id);
829
+ }
830
+ }
831
+ tierStats.tierA.selected = selected.filter((item) => item.tier === "tierA").length;
832
+ tierStats.tierB.selected = selected.filter((item) => item.tier === "tierB").length;
833
+ tierStats.tierC.selected = selected.filter((item) => item.tier === "tierC").length;
529
834
  const papers = selected.map(({ row, score }) => ({
530
835
  id: row.id,
531
836
  title: row.title,
@@ -550,7 +855,233 @@ async function strictCoreFallbackSeed(args) {
550
855
  papers,
551
856
  corePapers,
552
857
  explorationTrace: traces,
553
- notes: `strict_core_backfill_seed selected=${selected.length} queries=${queries.length}`,
858
+ notes: `strict_core_backfill_seed selected=${selected.length} pool=${candidatePool} floor=${minCoreFloor} relevance_floor=${minRelevance} req_foundational=${args.requirements.foundationalFirst} req_avoid_benchmark=${args.requirements.avoidBenchmarkOnly} req_survey=${args.requirements.preferSurvey}`,
859
+ recallTierStats: tierStats,
860
+ };
861
+ }
862
+ function isPaperFullTextRead(paper) {
863
+ return paper.fullTextRead === true || paper.readStatus === "fulltext";
864
+ }
865
+ function hasStrictEvidenceAnchor(paper) {
866
+ const anchors = paper.evidenceAnchors ?? [];
867
+ return anchors.some((anchor) => Boolean(anchor?.section?.trim()) &&
868
+ Boolean(anchor?.locator?.trim()) &&
869
+ Boolean(anchor?.quote?.trim()));
870
+ }
871
+ function firstNonEmptyText(values) {
872
+ for (const value of values) {
873
+ if (typeof value !== "string")
874
+ continue;
875
+ const normalized = normalizeText(value);
876
+ if (normalized.length > 0)
877
+ return normalized;
878
+ }
879
+ return undefined;
880
+ }
881
+ function toEvidencePaperId(paper) {
882
+ return derivePaperId({ id: paper.id, title: paper.title, url: paper.url });
883
+ }
884
+ function dedupeEvidenceIds(ids) {
885
+ const seen = new Set();
886
+ const out = [];
887
+ for (const id of ids) {
888
+ const normalized = normalizeText(id);
889
+ if (!normalized)
890
+ continue;
891
+ const key = normalized.toLowerCase();
892
+ if (seen.has(key))
893
+ continue;
894
+ seen.add(key);
895
+ out.push(normalized);
896
+ }
897
+ return out;
898
+ }
899
+ function applyLightweightEvidenceBinding(args) {
900
+ if (!args.knowledgeState) {
901
+ return { knowledgeState: args.knowledgeState, anchorsAdded: 0, evidenceIdsFilled: 0 };
902
+ }
903
+ const corePapers = args.knowledgeState.corePapers ?? [];
904
+ if (corePapers.length === 0) {
905
+ return { knowledgeState: args.knowledgeState, anchorsAdded: 0, evidenceIdsFilled: 0 };
906
+ }
907
+ let anchorsAdded = 0;
908
+ const nextCore = corePapers.map((paper) => {
909
+ if (!isPaperFullTextRead(paper))
910
+ return paper;
911
+ if (hasStrictEvidenceAnchor(paper))
912
+ return paper;
913
+ const quote = firstNonEmptyText([
914
+ paper.keyEvidenceSpans?.[0],
915
+ paper.summary,
916
+ paper.reason,
917
+ paper.title,
918
+ ]);
919
+ if (!quote)
920
+ return paper;
921
+ const nextQuote = quote.slice(0, 260);
922
+ anchorsAdded += 1;
923
+ return {
924
+ ...paper,
925
+ evidenceAnchors: [
926
+ ...(paper.evidenceAnchors ?? []),
927
+ {
928
+ section: "AutoExtract",
929
+ locator: paper.fullTextRef?.trim() || "excerpt:1",
930
+ claim: firstNonEmptyText([paper.researchGoal, paper.reason, paper.title, "auto-bound claim"]) ?? "auto-bound claim",
931
+ quote: nextQuote,
932
+ },
933
+ ],
934
+ };
935
+ });
936
+ const fallbackEvidenceIds = dedupeEvidenceIds(nextCore.filter((paper) => isPaperFullTextRead(paper)).map((paper) => toEvidencePaperId(paper)).slice(0, 2));
937
+ let evidenceIdsFilled = 0;
938
+ const patchEvidenceIds = (raw, allowAuto = true) => {
939
+ const existing = dedupeEvidenceIds(raw ?? []);
940
+ if (existing.length > 0)
941
+ return existing;
942
+ if (!allowAuto || fallbackEvidenceIds.length === 0)
943
+ return undefined;
944
+ evidenceIdsFilled += 1;
945
+ return [...fallbackEvidenceIds];
946
+ };
947
+ const nextKnowledgeChanges = (args.knowledgeState.knowledgeChanges ?? []).map((change) => ({
948
+ ...change,
949
+ ...(change.type === "BRIDGE"
950
+ ? { evidenceIds: patchEvidenceIds(change.evidenceIds, false) }
951
+ : { evidenceIds: patchEvidenceIds(change.evidenceIds, true) }),
952
+ }));
953
+ const nextKnowledgeUpdates = (args.knowledgeState.knowledgeUpdates ?? []).map((update) => ({
954
+ ...update,
955
+ evidenceIds: patchEvidenceIds(update.evidenceIds, true),
956
+ }));
957
+ const nextHypotheses = (args.knowledgeState.hypotheses ?? []).map((hypothesis) => ({
958
+ ...hypothesis,
959
+ evidenceIds: patchEvidenceIds(hypothesis.evidenceIds, true),
960
+ }));
961
+ if (anchorsAdded === 0 && evidenceIdsFilled === 0) {
962
+ return { knowledgeState: args.knowledgeState, anchorsAdded: 0, evidenceIdsFilled: 0 };
963
+ }
964
+ const existingRunLog = args.knowledgeState.runLog;
965
+ const runLog = existingRunLog || args.runProfile
966
+ ? {
967
+ ...(existingRunLog ?? {}),
968
+ ...(existingRunLog?.runProfile ? {} : args.runProfile ? { runProfile: args.runProfile } : {}),
969
+ notes: [existingRunLog?.notes, `auto_evidence_binding anchors_added=${anchorsAdded} ids_filled=${evidenceIdsFilled}`]
970
+ .filter((item) => Boolean(item && item.trim().length > 0))
971
+ .join(" || "),
972
+ }
973
+ : undefined;
974
+ return {
975
+ knowledgeState: {
976
+ ...args.knowledgeState,
977
+ corePapers: nextCore,
978
+ ...(nextKnowledgeChanges.length > 0 ? { knowledgeChanges: nextKnowledgeChanges } : {}),
979
+ ...(nextKnowledgeUpdates.length > 0 ? { knowledgeUpdates: nextKnowledgeUpdates } : {}),
980
+ ...(nextHypotheses.length > 0 ? { hypotheses: nextHypotheses } : {}),
981
+ ...(runLog ? { runLog } : {}),
982
+ },
983
+ anchorsAdded,
984
+ evidenceIdsFilled,
985
+ };
986
+ }
987
+ function buildReflectionFollowupQuery(topic, hint) {
988
+ const tokens = tokenizeKeywords(`${topic} ${hint}`).slice(0, 8);
989
+ if (tokens.length === 0)
990
+ return normalizeText(topic);
991
+ return tokens.join(" ");
992
+ }
993
+ function resolveSingleStepReflectionSeed(args) {
994
+ const changes = args.knowledgeState?.knowledgeChanges ?? [];
995
+ const bridgeChanges = changes.filter((item) => item.type === "BRIDGE");
996
+ const newChanges = changes.filter((item) => item.type === "NEW");
997
+ const reviseChanges = changes.filter((item) => item.type === "REVISE");
998
+ const unreadCore = (args.knowledgeState?.corePapers ?? []).filter((paper) => !isPaperFullTextRead(paper));
999
+ if (bridgeChanges.length > 0) {
1000
+ const seed = bridgeChanges[0]?.statement ?? args.topic;
1001
+ return {
1002
+ trigger: "BRIDGE",
1003
+ reason: "bridge_followup",
1004
+ query: buildReflectionFollowupQuery(args.topic, seed),
1005
+ };
1006
+ }
1007
+ if (newChanges.length >= 2 && reviseChanges.length >= 1) {
1008
+ const seed = `${newChanges[0]?.statement ?? ""} ${reviseChanges[0]?.statement ?? ""}`.trim();
1009
+ return {
1010
+ trigger: "CONFLICT",
1011
+ reason: "new_revise_followup",
1012
+ query: buildReflectionFollowupQuery(args.topic, seed || args.topic),
1013
+ };
1014
+ }
1015
+ if (unreadCore.length > 0) {
1016
+ const seed = unreadCore[0]?.id ?? unreadCore[0]?.title ?? args.topic;
1017
+ return {
1018
+ trigger: "UNREAD_CORE",
1019
+ reason: "unread_core_followup",
1020
+ query: buildReflectionFollowupQuery(args.topic, seed),
1021
+ };
1022
+ }
1023
+ return undefined;
1024
+ }
1025
+ async function executeSingleStepReflection(args) {
1026
+ const seed = resolveSingleStepReflectionSeed({
1027
+ topic: args.topic,
1028
+ knowledgeState: args.knowledgeState,
1029
+ });
1030
+ if (!seed) {
1031
+ return {
1032
+ executed: false,
1033
+ resultCount: 0,
1034
+ papers: [],
1035
+ changes: [],
1036
+ };
1037
+ }
1038
+ const rows = await fetchArxivFallbackByQuery(seed.query);
1039
+ const localKnownIds = new Set(args.knownPaperIds);
1040
+ for (const paper of args.effectivePapers) {
1041
+ localKnownIds.add(derivePaperId(paper));
1042
+ }
1043
+ for (const paper of args.knowledgeState?.corePapers ?? []) {
1044
+ localKnownIds.add(derivePaperId({ id: paper.id, title: paper.title, url: paper.url }));
1045
+ }
1046
+ for (const paper of args.knowledgeState?.explorationPapers ?? []) {
1047
+ localKnownIds.add(derivePaperId({ id: paper.id, title: paper.title, url: paper.url }));
1048
+ }
1049
+ const selected = rows.filter((row) => !localKnownIds.has(row.id)).slice(0, REFLECTION_MAX_ADDED_PAPERS);
1050
+ const papers = selected.map((row) => ({
1051
+ id: row.id,
1052
+ title: row.title,
1053
+ url: row.url,
1054
+ source: "arxiv",
1055
+ ...(row.published ? { publishedAt: row.published } : {}),
1056
+ ...(row.summary ? { summary: row.summary } : {}),
1057
+ fullTextRead: false,
1058
+ readStatus: "metadata",
1059
+ unreadReason: "single_step_reflection_added_without_fulltext",
1060
+ }));
1061
+ const changes = selected.length > 0
1062
+ ? [
1063
+ {
1064
+ type: "NEW",
1065
+ statement: `Reflection follow-up added ${selected.length} adjacent paper(s) for ${args.topic}.`,
1066
+ evidenceIds: selected.map((row) => row.id).slice(0, 3),
1067
+ topic: args.topic,
1068
+ },
1069
+ ]
1070
+ : [];
1071
+ return {
1072
+ executed: true,
1073
+ resultCount: selected.length,
1074
+ trace: {
1075
+ query: seed.query,
1076
+ reason: seed.reason,
1077
+ source: "arxiv",
1078
+ candidates: rows.length,
1079
+ filteredTo: selected.length,
1080
+ ...(selected.length === 0 ? { filteredOutReasons: ["no_unseen_reflection_candidates"] } : {}),
1081
+ resultCount: selected.length,
1082
+ },
1083
+ papers,
1084
+ changes,
554
1085
  };
555
1086
  }
556
1087
  function dedupePaperRecords(records) {
@@ -943,7 +1474,7 @@ export async function recordIncrementalPush(args) {
943
1474
  effectiveRunLog.requiredCorePapers = Math.max(1, requiredCoreRaw);
944
1475
  }
945
1476
  else {
946
- delete effectiveRunLog.requiredCorePapers;
1477
+ effectiveRunLog.requiredCorePapers = Math.max(1, Math.min(topicState.preferences.maxPapers, DEFAULT_STRICT_MIN_CORE_FLOOR));
947
1478
  }
948
1479
  if (typeof effectiveRunLog.requiredFullTextCoveragePct !== "number" ||
949
1480
  !Number.isFinite(effectiveRunLog.requiredFullTextCoveragePct) ||
@@ -957,8 +1488,18 @@ export async function recordIncrementalPush(args) {
957
1488
  ...(effectiveRunLog ? { runLog: effectiveRunLog } : {}),
958
1489
  }
959
1490
  : undefined;
1491
+ const requirementProfile = inferRequirementProfile([
1492
+ topicState.topic,
1493
+ args.note,
1494
+ effectiveRunLog?.notes,
1495
+ effectiveKnowledgeState?.runLog?.notes,
1496
+ ]
1497
+ .filter((item) => Boolean(item && item.trim().length > 0))
1498
+ .join(" "));
960
1499
  if (incomingRunProfile === "strict") {
961
- const requiredCoreFloor = Math.max(1, Math.min(topicState.preferences.maxPapers, effectiveRunLog?.requiredCorePapers ?? Math.min(3, topicState.preferences.maxPapers)));
1500
+ const strictMinCoreFloor = Math.max(1, Math.min(topicState.preferences.maxPapers, DEFAULT_STRICT_MIN_CORE_FLOOR));
1501
+ const requiredCoreFloor = Math.max(1, Math.min(topicState.preferences.maxPapers, effectiveRunLog?.requiredCorePapers ?? strictMinCoreFloor));
1502
+ const strictCandidatePool = Math.max(DEFAULT_STRICT_CANDIDATE_POOL, topicState.preferences.maxPapers * 4);
962
1503
  const existingCorePapers = effectiveKnowledgeState?.corePapers ?? [];
963
1504
  const strictSignalCount = Math.max(existingCorePapers.length, effectivePapers.length);
964
1505
  if (strictSignalCount < requiredCoreFloor) {
@@ -970,8 +1511,11 @@ export async function recordIncrementalPush(args) {
970
1511
  }
971
1512
  const fallback = await strictCoreFallbackSeed({
972
1513
  topic: topicState.topic,
973
- maxPapers: requiredCoreFloor,
1514
+ maxPapers: topicState.preferences.maxPapers,
1515
+ candidatePool: strictCandidatePool,
1516
+ minCoreFloor: requiredCoreFloor,
974
1517
  knownPaperIds: knownIds,
1518
+ requirements: requirementProfile,
975
1519
  });
976
1520
  if (fallback.papers.length > 0) {
977
1521
  const existingIds = new Set(effectivePapers.map((paper) => derivePaperId(paper)));
@@ -987,6 +1531,7 @@ export async function recordIncrementalPush(args) {
987
1531
  effectivePapers = dedupePaperRecords([...effectivePapers, ...fallbackPapers]);
988
1532
  const mergedRunLog = {
989
1533
  ...(effectiveRunLog ?? { runProfile: "strict" }),
1534
+ recallTierStats: fallback.recallTierStats,
990
1535
  notes: [
991
1536
  effectiveRunLog?.notes,
992
1537
  fallback.notes,
@@ -1050,6 +1595,74 @@ export async function recordIncrementalPush(args) {
1050
1595
  };
1051
1596
  }
1052
1597
  }
1598
+ const reflection = await executeSingleStepReflection({
1599
+ topic: topicState.topic,
1600
+ knownPaperIds: new Set(Object.keys(topicState.pushedPapers)),
1601
+ effectivePapers,
1602
+ knowledgeState: effectiveKnowledgeState,
1603
+ });
1604
+ const reflectionRunLogBase = effectiveRunLog ??
1605
+ (incomingRunProfile ? { runProfile: incomingRunProfile } : undefined);
1606
+ if (reflection.executed) {
1607
+ const reflectionPaperRecords = reflection.papers.map((paper) => ({
1608
+ ...(paper.id ? { id: paper.id } : {}),
1609
+ ...(paper.title ? { title: paper.title } : {}),
1610
+ ...(paper.url ? { url: paper.url } : {}),
1611
+ ...(typeof paper.score === "number" && Number.isFinite(paper.score) ? { score: paper.score } : {}),
1612
+ reason: "single_step_reflection_followup",
1613
+ }));
1614
+ effectivePapers = dedupePaperRecords([...effectivePapers, ...reflectionPaperRecords]);
1615
+ const mergedRunLog = {
1616
+ ...(reflectionRunLogBase ?? {}),
1617
+ reflectionStepExecuted: true,
1618
+ reflectionStepResultCount: reflection.resultCount,
1619
+ notes: [
1620
+ reflectionRunLogBase?.notes,
1621
+ `single_step_reflection result_count=${reflection.resultCount}`,
1622
+ ]
1623
+ .filter((item) => Boolean(item && item.trim().length > 0))
1624
+ .join(" || "),
1625
+ };
1626
+ effectiveRunLog = mergedRunLog;
1627
+ effectiveKnowledgeState = {
1628
+ ...(effectiveKnowledgeState ?? {}),
1629
+ explorationTrace: [
1630
+ ...(effectiveKnowledgeState?.explorationTrace ?? []),
1631
+ ...(reflection.trace ? [reflection.trace] : []),
1632
+ ],
1633
+ explorationPapers: dedupeKnowledgePapers([
1634
+ ...(effectiveKnowledgeState?.explorationPapers ?? []),
1635
+ ...reflection.papers,
1636
+ ]),
1637
+ knowledgeChanges: [
1638
+ ...(effectiveKnowledgeState?.knowledgeChanges ?? []),
1639
+ ...(reflection.changes ?? []),
1640
+ ],
1641
+ runLog: mergedRunLog,
1642
+ };
1643
+ }
1644
+ else if (reflectionRunLogBase) {
1645
+ const mergedRunLog = {
1646
+ ...reflectionRunLogBase,
1647
+ reflectionStepExecuted: false,
1648
+ reflectionStepResultCount: 0,
1649
+ };
1650
+ effectiveRunLog = mergedRunLog;
1651
+ effectiveKnowledgeState = {
1652
+ ...(effectiveKnowledgeState ?? {}),
1653
+ runLog: mergedRunLog,
1654
+ };
1655
+ }
1656
+ const autoEvidence = applyLightweightEvidenceBinding({
1657
+ knowledgeState: effectiveKnowledgeState,
1658
+ runProfile: incomingRunProfile,
1659
+ });
1660
+ effectiveKnowledgeState = autoEvidence.knowledgeState;
1661
+ if (autoEvidence.anchorsAdded > 0 || autoEvidence.evidenceIdsFilled > 0) {
1662
+ effectiveRunLog = effectiveKnowledgeState?.runLog
1663
+ ? { ...effectiveKnowledgeState.runLog }
1664
+ : effectiveRunLog;
1665
+ }
1053
1666
  const statusRaw = normalizeText(args.status ?? "").toLowerCase();
1054
1667
  const researchArtifactsCount = effectivePapers.length +
1055
1668
  (effectiveKnowledgeState?.explorationPapers?.length ?? 0) +