scientify 1.13.5 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.en.md +350 -0
  2. package/README.md +148 -358
  3. package/dist/index.d.ts +8 -2
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +131 -122
  6. package/dist/index.js.map +1 -1
  7. package/dist/src/cli/research.d.ts +1 -6
  8. package/dist/src/cli/research.d.ts.map +1 -1
  9. package/dist/src/cli/research.js +227 -123
  10. package/dist/src/cli/research.js.map +1 -1
  11. package/dist/src/commands/metabolism-status.d.ts +3 -3
  12. package/dist/src/commands/metabolism-status.d.ts.map +1 -1
  13. package/dist/src/commands/metabolism-status.js +72 -75
  14. package/dist/src/commands/metabolism-status.js.map +1 -1
  15. package/dist/src/commands.d.ts +1 -1
  16. package/dist/src/commands.d.ts.map +1 -1
  17. package/dist/src/commands.js +0 -55
  18. package/dist/src/commands.js.map +1 -1
  19. package/dist/src/hooks/cron-skill-inject.d.ts +6 -7
  20. package/dist/src/hooks/cron-skill-inject.d.ts.map +1 -1
  21. package/dist/src/hooks/cron-skill-inject.js +6 -15
  22. package/dist/src/hooks/cron-skill-inject.js.map +1 -1
  23. package/dist/src/hooks/research-mode.d.ts +1 -1
  24. package/dist/src/hooks/research-mode.d.ts.map +1 -1
  25. package/dist/src/hooks/research-mode.js +24 -72
  26. package/dist/src/hooks/research-mode.js.map +1 -1
  27. package/dist/src/hooks/scientify-signature.d.ts +1 -1
  28. package/dist/src/hooks/scientify-signature.d.ts.map +1 -1
  29. package/dist/src/hooks/scientify-signature.js +2 -5
  30. package/dist/src/hooks/scientify-signature.js.map +1 -1
  31. package/dist/src/knowledge-state/render.d.ts +1 -9
  32. package/dist/src/knowledge-state/render.d.ts.map +1 -1
  33. package/dist/src/knowledge-state/render.js +33 -158
  34. package/dist/src/knowledge-state/render.js.map +1 -1
  35. package/dist/src/knowledge-state/store.d.ts.map +1 -1
  36. package/dist/src/knowledge-state/store.js +65 -884
  37. package/dist/src/knowledge-state/store.js.map +1 -1
  38. package/dist/src/knowledge-state/types.d.ts +0 -69
  39. package/dist/src/knowledge-state/types.d.ts.map +1 -1
  40. package/dist/src/literature/subscription-state.d.ts +0 -2
  41. package/dist/src/literature/subscription-state.d.ts.map +1 -1
  42. package/dist/src/literature/subscription-state.js +7 -1199
  43. package/dist/src/literature/subscription-state.js.map +1 -1
  44. package/dist/src/research-subscriptions/constants.d.ts +1 -1
  45. package/dist/src/research-subscriptions/constants.js +1 -1
  46. package/dist/src/research-subscriptions/cron-client.d.ts +1 -1
  47. package/dist/src/research-subscriptions/cron-client.d.ts.map +1 -1
  48. package/dist/src/research-subscriptions/delivery.d.ts +1 -1
  49. package/dist/src/research-subscriptions/delivery.d.ts.map +1 -1
  50. package/dist/src/research-subscriptions/handlers.d.ts +1 -1
  51. package/dist/src/research-subscriptions/handlers.d.ts.map +1 -1
  52. package/dist/src/research-subscriptions/handlers.js +9 -9
  53. package/dist/src/research-subscriptions/handlers.js.map +1 -1
  54. package/dist/src/research-subscriptions/parse.d.ts.map +1 -1
  55. package/dist/src/research-subscriptions/parse.js +0 -10
  56. package/dist/src/research-subscriptions/parse.js.map +1 -1
  57. package/dist/src/research-subscriptions/prompt.d.ts +1 -1
  58. package/dist/src/research-subscriptions/prompt.d.ts.map +1 -1
  59. package/dist/src/research-subscriptions/prompt.js +196 -191
  60. package/dist/src/research-subscriptions/prompt.js.map +1 -1
  61. package/dist/src/research-subscriptions/types.d.ts +1 -2
  62. package/dist/src/research-subscriptions/types.d.ts.map +1 -1
  63. package/dist/src/templates/bootstrap.d.ts.map +1 -1
  64. package/dist/src/templates/bootstrap.js +32 -19
  65. package/dist/src/templates/bootstrap.js.map +1 -1
  66. package/dist/src/tools/arxiv-download.d.ts +1 -2
  67. package/dist/src/tools/arxiv-download.d.ts.map +1 -1
  68. package/dist/src/tools/arxiv-search.d.ts +1 -2
  69. package/dist/src/tools/arxiv-search.d.ts.map +1 -1
  70. package/dist/src/tools/github-search-tool.d.ts +1 -2
  71. package/dist/src/tools/github-search-tool.d.ts.map +1 -1
  72. package/dist/src/tools/openalex-search.d.ts +1 -2
  73. package/dist/src/tools/openalex-search.d.ts.map +1 -1
  74. package/dist/src/tools/openreview-lookup.d.ts +1 -2
  75. package/dist/src/tools/openreview-lookup.d.ts.map +1 -1
  76. package/dist/src/tools/paper-browser.d.ts +1 -2
  77. package/dist/src/tools/paper-browser.d.ts.map +1 -1
  78. package/dist/src/tools/result.d.ts +3 -5
  79. package/dist/src/tools/result.d.ts.map +1 -1
  80. package/dist/src/tools/result.js +5 -7
  81. package/dist/src/tools/result.js.map +1 -1
  82. package/dist/src/tools/scientify-cron.d.ts +4 -9
  83. package/dist/src/tools/scientify-cron.d.ts.map +1 -1
  84. package/dist/src/tools/scientify-cron.js +19 -441
  85. package/dist/src/tools/scientify-cron.js.map +1 -1
  86. package/dist/src/tools/scientify-literature-state.d.ts +1 -62
  87. package/dist/src/tools/scientify-literature-state.d.ts.map +1 -1
  88. package/dist/src/tools/scientify-literature-state.js +46 -312
  89. package/dist/src/tools/scientify-literature-state.js.map +1 -1
  90. package/dist/src/tools/unpaywall-download.d.ts +1 -2
  91. package/dist/src/tools/unpaywall-download.d.ts.map +1 -1
  92. package/dist/src/types.d.ts +16 -0
  93. package/dist/src/types.d.ts.map +1 -0
  94. package/dist/src/types.js +2 -0
  95. package/dist/src/types.js.map +1 -0
  96. package/openclaw.plugin.json +4 -2
  97. package/package.json +1 -1
  98. package/skills/metabolism/SKILL.md +2 -0
  99. package/skills/research-subscription/SKILL.md +1 -29
  100. package/README.zh.md +0 -494
@@ -9,19 +9,6 @@ const DEFAULT_SOURCES = ["openalex", "arxiv"];
9
9
  const MAX_MEMORY_NOTES = 30;
10
10
  const MAX_MEMORY_KEYS = 60;
11
11
  const TOP_HINT_LIMIT = 8;
12
- const DEFAULT_FULLTEXT_FETCH_TIMEOUT_MS = 20_000;
13
- const RETRY_FULLTEXT_FETCH_TIMEOUT_MS = 35_000;
14
- const MIN_FULLTEXT_TEXT_CHARS = 2_000;
15
- const MAX_STRICT_FULLTEXT_ATTEMPTS = 5;
16
- const ARXIV_API_URL = "https://export.arxiv.org/api/query";
17
- const STRICT_EMPTY_FALLBACK_MAX_RESULTS = 12;
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;
25
12
  const FEEDBACK_SIGNAL_DELTA = {
26
13
  read: 1,
27
14
  skip: -1,
@@ -184,201 +171,6 @@ function derivePaperId(paper) {
184
171
  const digest = createHash("sha1").update(fallback || JSON.stringify(paper)).digest("hex");
185
172
  return `hash:${digest.slice(0, 20)}`;
186
173
  }
187
- function normalizeArxivToken(token) {
188
- const cleaned = normalizeText(token).replace(/^arxiv:/i, "");
189
- if (!cleaned)
190
- return undefined;
191
- const modern = cleaned.match(/^(\d{4}\.\d{4,5}(?:v\d+)?)$/i);
192
- if (modern?.[1])
193
- return modern[1].toLowerCase();
194
- const legacy = cleaned.match(/^([a-z\-]+(?:\.[a-z\-]+)?\/\d{7}(?:v\d+)?)$/i);
195
- if (legacy?.[1])
196
- return legacy[1].toLowerCase();
197
- return undefined;
198
- }
199
- function stripArxivVersion(id) {
200
- return id.replace(/v\d+$/i, "");
201
- }
202
- function parseArxivIdCandidatesFromPaper(paper) {
203
- const candidates = [];
204
- const pushToken = (value) => {
205
- if (!value)
206
- return;
207
- const normalized = normalizeArxivToken(value);
208
- if (normalized)
209
- candidates.push(normalized);
210
- };
211
- pushToken(paper.id);
212
- const combined = [paper.url, paper.title].filter((item) => Boolean(item)).join(" ");
213
- for (const m of combined.matchAll(/\b(\d{4}\.\d{4,5}(?:v\d+)?)\b/gi)) {
214
- pushToken(m[1]);
215
- }
216
- for (const m of combined.matchAll(/\b([a-z\-]+(?:\.[a-z\-]+)?\/\d{7}(?:v\d+)?)\b/gi)) {
217
- pushToken(m[1]);
218
- }
219
- const expanded = [];
220
- const seen = new Set();
221
- for (const item of candidates) {
222
- if (!seen.has(item)) {
223
- seen.add(item);
224
- expanded.push(item);
225
- }
226
- const base = stripArxivVersion(item);
227
- if (!seen.has(base)) {
228
- seen.add(base);
229
- expanded.push(base);
230
- }
231
- }
232
- return expanded;
233
- }
234
- function htmlToPlainText(html) {
235
- return html
236
- .replace(/<script[\s\S]*?<\/script>/gi, " ")
237
- .replace(/<style[\s\S]*?<\/style>/gi, " ")
238
- .replace(/<noscript[\s\S]*?<\/noscript>/gi, " ")
239
- .replace(/<svg[\s\S]*?<\/svg>/gi, " ")
240
- .replace(/<math[\s\S]*?<\/math>/gi, " ")
241
- .replace(/<\/?(?:p|div|section|article|h\d|li|ul|ol|br|tr|td|th|table|blockquote)[^>]*>/gi, "\n")
242
- .replace(/<[^>]+>/g, " ")
243
- .replace(/&nbsp;/gi, " ")
244
- .replace(/&amp;/gi, "&")
245
- .replace(/&lt;/gi, "<")
246
- .replace(/&gt;/gi, ">")
247
- .replace(/&quot;/gi, "\"")
248
- .replace(/&#39;/gi, "'")
249
- .replace(/\r/g, "")
250
- .replace(/[ \t]+\n/g, "\n")
251
- .replace(/\n{3,}/g, "\n\n")
252
- .replace(/[ \t]{2,}/g, " ")
253
- .trim();
254
- }
255
- async function fetchArxivFullTextByHtmlCandidates(arxivIds, timeoutMs) {
256
- const candidates = [];
257
- const seen = new Set();
258
- for (const id of arxivIds) {
259
- const normalized = normalizeArxivToken(id);
260
- if (!normalized)
261
- continue;
262
- for (const host of ["https://arxiv.org/html", "https://ar5iv.org/html"]) {
263
- const url = `${host}/${normalized}`;
264
- if (seen.has(url))
265
- continue;
266
- seen.add(url);
267
- candidates.push({ url, tag: host.includes("ar5iv") ? "ar5iv_html" : "arxiv_html" });
268
- }
269
- }
270
- const errors = [];
271
- for (const candidate of candidates) {
272
- const controller = new AbortController();
273
- const timer = setTimeout(() => controller.abort(), timeoutMs);
274
- try {
275
- const res = await fetch(candidate.url, {
276
- signal: controller.signal,
277
- headers: {
278
- "User-Agent": "scientify-fulltext-bootstrap/1.0",
279
- },
280
- });
281
- if (!res.ok) {
282
- errors.push(`${candidate.tag}:http_${res.status}`);
283
- continue;
284
- }
285
- const rawHtml = await res.text();
286
- const plain = htmlToPlainText(rawHtml);
287
- if (plain.length < MIN_FULLTEXT_TEXT_CHARS) {
288
- errors.push(`${candidate.tag}:content_too_short(${plain.length})`);
289
- continue;
290
- }
291
- return {
292
- ok: true,
293
- sourceUrl: candidate.url,
294
- sourceTag: candidate.tag,
295
- plainText: plain,
296
- };
297
- }
298
- catch (error) {
299
- errors.push(`${candidate.tag}:${error instanceof Error ? error.name || error.message : "fetch_failed"}`);
300
- }
301
- finally {
302
- clearTimeout(timer);
303
- }
304
- }
305
- return {
306
- ok: false,
307
- reason: errors.length > 0 ? errors.join(";") : "html_fulltext_unavailable",
308
- };
309
- }
310
- async function backfillStrictCoreFullText(args) {
311
- const updated = [];
312
- let attempted = 0;
313
- let completed = 0;
314
- const failures = [];
315
- for (const paper of args.corePapers) {
316
- if (paper.fullTextRead === true || paper.readStatus === "fulltext") {
317
- updated.push(paper);
318
- continue;
319
- }
320
- const arxivIds = parseArxivIdCandidatesFromPaper({
321
- id: paper.id,
322
- url: paper.url,
323
- title: paper.title,
324
- });
325
- if (arxivIds.length === 0) {
326
- updated.push({
327
- ...paper,
328
- fullTextRead: false,
329
- readStatus: paper.readStatus ?? "metadata",
330
- unreadReason: paper.unreadReason ??
331
- "Automatic full-text bootstrap currently supports arXiv papers with parseable IDs only.",
332
- });
333
- continue;
334
- }
335
- if (attempted >= args.maxAttempts) {
336
- updated.push({
337
- ...paper,
338
- fullTextRead: false,
339
- readStatus: paper.readStatus ?? "metadata",
340
- unreadReason: paper.unreadReason ?? "Full-text bootstrap attempt budget reached in this run.",
341
- });
342
- continue;
343
- }
344
- attempted += 1;
345
- let fetched = await fetchArxivFullTextByHtmlCandidates(arxivIds, DEFAULT_FULLTEXT_FETCH_TIMEOUT_MS);
346
- if (!fetched.ok) {
347
- fetched = await fetchArxivFullTextByHtmlCandidates(arxivIds, RETRY_FULLTEXT_FETCH_TIMEOUT_MS);
348
- }
349
- if (!fetched.ok) {
350
- failures.push(`${arxivIds[0]}:${fetched.reason}`);
351
- updated.push({
352
- ...paper,
353
- fullTextRead: false,
354
- readStatus: paper.readStatus ?? "metadata",
355
- unreadReason: paper.unreadReason ?? `Automatic full-text fetch failed: ${fetched.reason}`,
356
- });
357
- continue;
358
- }
359
- completed += 1;
360
- const excerpt = fetched.plainText.slice(0, 360).replace(/\s+/g, " ").trim();
361
- updated.push({
362
- ...paper,
363
- fullTextRead: true,
364
- readStatus: "fulltext",
365
- fullTextSource: fetched.sourceTag,
366
- fullTextRef: fetched.sourceUrl,
367
- unreadReason: undefined,
368
- ...(paper.keyEvidenceSpans && paper.keyEvidenceSpans.length > 0
369
- ? {}
370
- : excerpt.length > 0
371
- ? { keyEvidenceSpans: [excerpt] }
372
- : {}),
373
- });
374
- }
375
- return {
376
- corePapers: updated,
377
- attempted,
378
- completed,
379
- failures,
380
- };
381
- }
382
174
  function sanitizeKeyword(raw) {
383
175
  const normalized = normalizeText(raw).toLowerCase();
384
176
  if (normalized.length < 2 || normalized.length > 48)
@@ -396,741 +188,6 @@ function tokenizeKeywords(raw) {
396
188
  }
397
189
  return [...seen];
398
190
  }
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;
491
- function decodeXmlEntities(raw) {
492
- return raw
493
- .replace(/&lt;/g, "<")
494
- .replace(/&gt;/g, ">")
495
- .replace(/&amp;/g, "&")
496
- .replace(/&quot;/g, "\"")
497
- .replace(/&apos;/g, "'");
498
- }
499
- function stripXmlTag(raw, tag) {
500
- const match = raw.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, "i"));
501
- if (!match?.[1])
502
- return "";
503
- return normalizeText(decodeXmlEntities(match[1].replace(/<[^>]+>/g, " ").trim()));
504
- }
505
- function parseArxivAtomCandidates(xml) {
506
- const entries = xml.match(/<entry>([\s\S]*?)<\/entry>/gi) ?? [];
507
- const parsed = [];
508
- for (const entryRaw of entries) {
509
- const title = stripXmlTag(entryRaw, "title");
510
- const summary = stripXmlTag(entryRaw, "summary");
511
- const idUrl = stripXmlTag(entryRaw, "id");
512
- const published = stripXmlTag(entryRaw, "published");
513
- const arxivCandidates = parseArxivIdCandidatesFromPaper({ id: idUrl, url: idUrl, title });
514
- const arxivId = arxivCandidates[0];
515
- if (!title || !arxivId)
516
- continue;
517
- parsed.push({
518
- id: `arxiv:${stripArxivVersion(arxivId)}`,
519
- title,
520
- summary,
521
- url: `https://arxiv.org/abs/${stripArxivVersion(arxivId)}`,
522
- ...(published ? { published } : {}),
523
- });
524
- }
525
- return parsed;
526
- }
527
- function dedupeQueries(queries, limit) {
528
- const seen = new Set();
529
- const deduped = [];
530
- for (const query of queries) {
531
- const key = normalizeText(query).toLowerCase();
532
- if (!key || seen.has(key))
533
- continue;
534
- seen.add(key);
535
- deduped.push(query);
536
- if (deduped.length >= limit)
537
- break;
538
- }
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;
614
- }
615
- function countTokenOverlap(tokens, text) {
616
- const hay = ` ${normalizeText(text)
617
- .toLowerCase()
618
- .replace(/[_-]+/g, " ")
619
- .replace(/[^\p{L}\p{N}\s]+/gu, " ")
620
- .replace(/\s+/g, " ")} `;
621
- let score = 0;
622
- for (const token of tokens) {
623
- if (token.length < 2)
624
- continue;
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} `))
633
- score += 1;
634
- }
635
- return score;
636
- }
637
- function scoreFallbackCandidate(topicTokens, paper, tier, requirements) {
638
- const titleOverlap = countTokenOverlap(topicTokens, paper.title);
639
- const abstractOverlap = countTokenOverlap(topicTokens, paper.summary ?? "");
640
- const publishedAt = paper.published ? Date.parse(paper.published) : NaN;
641
- const recencyBoost = Number.isFinite(publishedAt)
642
- ? Math.max(0, Math.min(8, (Date.now() - publishedAt) / (1000 * 60 * 60 * 24 * -180)))
643
- : 0;
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
- }
669
- return Math.max(50, Math.min(99, Math.round(rawScore)));
670
- }
671
- async function fetchArxivFallbackByQuery(query) {
672
- const params = new URLSearchParams({
673
- search_query: query,
674
- start: "0",
675
- max_results: String(STRICT_EMPTY_FALLBACK_MAX_RESULTS),
676
- sortBy: "relevance",
677
- sortOrder: "descending",
678
- });
679
- const controller = new AbortController();
680
- const timer = setTimeout(() => controller.abort(), 15_000);
681
- try {
682
- const res = await fetch(`${ARXIV_API_URL}?${params.toString()}`, {
683
- signal: controller.signal,
684
- headers: {
685
- "User-Agent": "scientify-empty-fallback/1.0",
686
- },
687
- });
688
- if (!res.ok)
689
- return [];
690
- const xml = await res.text();
691
- return parseArxivAtomCandidates(xml);
692
- }
693
- catch {
694
- return [];
695
- }
696
- finally {
697
- clearTimeout(timer);
698
- }
699
- }
700
- async function strictCoreFallbackSeed(args) {
701
- const tieredQueries = buildTieredFallbackQueries(args.topic);
702
- const byId = new Map();
703
- const traces = [];
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
- }
725
- }
726
- }
727
- const topicTokens = tokenizeKeywords(args.topic);
728
- const scoringTokens = buildScoringTokens(args.topic);
729
- const ranked = [...byId.values()]
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
- })
747
- .sort((a, b) => b.score - a.score);
748
- const unseen = ranked.filter((item) => !args.knownPaperIds.has(item.row.id));
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;
834
- const papers = selected.map(({ row, score }) => ({
835
- id: row.id,
836
- title: row.title,
837
- url: row.url,
838
- score,
839
- reason: "auto_seeded_fallback_after_sparse_core_strict_run",
840
- }));
841
- const corePapers = selected.map(({ row, score }) => ({
842
- id: row.id,
843
- title: row.title,
844
- url: row.url,
845
- source: "arxiv",
846
- ...(row.published ? { publishedAt: row.published } : {}),
847
- score,
848
- reason: "auto_seeded_fallback_after_sparse_core_strict_run",
849
- ...(row.summary ? { summary: row.summary } : {}),
850
- fullTextRead: false,
851
- readStatus: "metadata",
852
- unreadReason: "Auto-seeded fallback candidate; full-text bootstrap pending.",
853
- }));
854
- return {
855
- papers,
856
- corePapers,
857
- explorationTrace: traces,
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,
1085
- };
1086
- }
1087
- function dedupePaperRecords(records) {
1088
- const byId = new Map();
1089
- for (const record of records) {
1090
- const id = derivePaperId(record);
1091
- const existing = byId.get(id);
1092
- if (!existing) {
1093
- byId.set(id, { ...record, ...(record.id ? {} : { id }) });
1094
- continue;
1095
- }
1096
- byId.set(id, {
1097
- id: existing.id ?? record.id ?? id,
1098
- title: existing.title ?? record.title,
1099
- url: existing.url ?? record.url,
1100
- score: typeof existing.score === "number" && Number.isFinite(existing.score)
1101
- ? typeof record.score === "number" && Number.isFinite(record.score)
1102
- ? Math.max(existing.score, record.score)
1103
- : existing.score
1104
- : record.score,
1105
- reason: existing.reason ?? record.reason,
1106
- });
1107
- }
1108
- return [...byId.values()];
1109
- }
1110
- function dedupeKnowledgePapers(records) {
1111
- const byId = new Map();
1112
- for (const record of records) {
1113
- const id = derivePaperId({ id: record.id, title: record.title, url: record.url });
1114
- const existing = byId.get(id);
1115
- if (!existing) {
1116
- byId.set(id, {
1117
- ...record,
1118
- ...(record.id ? {} : { id }),
1119
- });
1120
- continue;
1121
- }
1122
- byId.set(id, {
1123
- ...existing,
1124
- ...record,
1125
- id: existing.id ?? record.id ?? id,
1126
- title: existing.title ?? record.title,
1127
- url: existing.url ?? record.url,
1128
- summary: existing.summary ?? record.summary,
1129
- unreadReason: existing.unreadReason ?? record.unreadReason,
1130
- });
1131
- }
1132
- return [...byId.values()];
1133
- }
1134
191
  function normalizeSource(raw) {
1135
192
  if (!raw)
1136
193
  return undefined;
@@ -1445,243 +502,8 @@ export async function recordIncrementalPush(args) {
1445
502
  const topicState = getOrCreateTopicState(root, args.scope, args.topic, args.preferences);
1446
503
  const memory = ensureTopicMemoryState(topicState);
1447
504
  const now = Date.now();
1448
- const normalizedPapersFromKnowledgeState = (args.knowledgeState?.corePapers ?? [])
1449
- .filter((paper) => paper && typeof paper === "object")
1450
- .map((paper) => ({
1451
- ...(paper.id ? { id: paper.id } : {}),
1452
- ...(paper.title ? { title: paper.title } : {}),
1453
- ...(paper.url ? { url: paper.url } : {}),
1454
- ...(typeof paper.score === "number" && Number.isFinite(paper.score) ? { score: paper.score } : {}),
1455
- ...(paper.reason ? { reason: paper.reason } : {}),
1456
- }));
1457
- let effectivePapers = args.papers.length > 0
1458
- ? args.papers
1459
- : normalizedPapersFromKnowledgeState.length > 0
1460
- ? normalizedPapersFromKnowledgeState
1461
- : [];
1462
- const incomingRunLog = args.knowledgeState?.runLog
1463
- ? { ...args.knowledgeState.runLog }
1464
- : undefined;
1465
- const incomingRunProfile = incomingRunLog?.runProfile === "fast" || incomingRunLog?.runProfile === "strict"
1466
- ? incomingRunLog.runProfile
1467
- : undefined;
1468
- let effectiveRunLog = incomingRunLog ? { ...incomingRunLog } : undefined;
1469
- if (incomingRunProfile === "strict" && effectiveRunLog) {
1470
- const requiredCoreRaw = typeof effectiveRunLog.requiredCorePapers === "number" && Number.isFinite(effectiveRunLog.requiredCorePapers)
1471
- ? Math.floor(effectiveRunLog.requiredCorePapers)
1472
- : 0;
1473
- if (requiredCoreRaw > 0) {
1474
- effectiveRunLog.requiredCorePapers = Math.max(1, requiredCoreRaw);
1475
- }
1476
- else {
1477
- effectiveRunLog.requiredCorePapers = Math.max(1, Math.min(topicState.preferences.maxPapers, DEFAULT_STRICT_MIN_CORE_FLOOR));
1478
- }
1479
- if (typeof effectiveRunLog.requiredFullTextCoveragePct !== "number" ||
1480
- !Number.isFinite(effectiveRunLog.requiredFullTextCoveragePct) ||
1481
- effectiveRunLog.requiredFullTextCoveragePct < 80) {
1482
- effectiveRunLog.requiredFullTextCoveragePct = 80;
1483
- }
1484
- }
1485
- let effectiveKnowledgeState = args.knowledgeState || effectiveRunLog
1486
- ? {
1487
- ...(args.knowledgeState ?? {}),
1488
- ...(effectiveRunLog ? { runLog: effectiveRunLog } : {}),
1489
- }
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(" "));
1499
- if (incomingRunProfile === "strict") {
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);
1503
- const existingCorePapers = effectiveKnowledgeState?.corePapers ?? [];
1504
- const strictSignalCount = Math.max(existingCorePapers.length, effectivePapers.length);
1505
- if (strictSignalCount < requiredCoreFloor) {
1506
- const knownIds = new Set(Object.keys(topicState.pushedPapers));
1507
- for (const paper of effectivePapers)
1508
- knownIds.add(derivePaperId(paper));
1509
- for (const paper of existingCorePapers) {
1510
- knownIds.add(derivePaperId({ id: paper.id, title: paper.title, url: paper.url }));
1511
- }
1512
- const fallback = await strictCoreFallbackSeed({
1513
- topic: topicState.topic,
1514
- maxPapers: topicState.preferences.maxPapers,
1515
- candidatePool: strictCandidatePool,
1516
- minCoreFloor: requiredCoreFloor,
1517
- knownPaperIds: knownIds,
1518
- requirements: requirementProfile,
1519
- });
1520
- if (fallback.papers.length > 0) {
1521
- const existingIds = new Set(effectivePapers.map((paper) => derivePaperId(paper)));
1522
- let fallbackPapers = fallback.papers.filter((paper) => !existingIds.has(derivePaperId(paper)));
1523
- const needed = Math.max(0, requiredCoreFloor - strictSignalCount);
1524
- if (needed > 0) {
1525
- if (fallbackPapers.length === 0)
1526
- fallbackPapers = fallback.papers;
1527
- fallbackPapers = fallbackPapers.slice(0, needed);
1528
- }
1529
- const fallbackIds = new Set(fallbackPapers.map((paper) => derivePaperId(paper)));
1530
- const fallbackCore = fallback.corePapers.filter((paper) => fallbackIds.has(derivePaperId({ id: paper.id, title: paper.title, url: paper.url })));
1531
- effectivePapers = dedupePaperRecords([...effectivePapers, ...fallbackPapers]);
1532
- const mergedRunLog = {
1533
- ...(effectiveRunLog ?? { runProfile: "strict" }),
1534
- recallTierStats: fallback.recallTierStats,
1535
- notes: [
1536
- effectiveRunLog?.notes,
1537
- fallback.notes,
1538
- `strict_core_topup required=${requiredCoreFloor} before=${strictSignalCount} added=${fallbackPapers.length}`,
1539
- ]
1540
- .filter((item) => Boolean(item && item.trim().length > 0))
1541
- .join(" || "),
1542
- };
1543
- effectiveRunLog = mergedRunLog;
1544
- effectiveKnowledgeState = {
1545
- ...(effectiveKnowledgeState ?? {}),
1546
- corePapers: dedupeKnowledgePapers([...(effectiveKnowledgeState?.corePapers ?? []), ...fallbackCore]),
1547
- explorationTrace: [
1548
- ...(effectiveKnowledgeState?.explorationTrace ?? []),
1549
- ...fallback.explorationTrace,
1550
- ],
1551
- runLog: mergedRunLog,
1552
- };
1553
- }
1554
- }
1555
- }
1556
- if (incomingRunProfile === "strict") {
1557
- const strictCoreFromState = effectiveKnowledgeState?.corePapers ?? [];
1558
- const strictCoreSeed = strictCoreFromState.length > 0
1559
- ? strictCoreFromState
1560
- : effectivePapers.map((paper) => ({
1561
- ...(paper.id ? { id: paper.id } : {}),
1562
- ...(paper.title ? { title: paper.title } : {}),
1563
- ...(paper.url ? { url: paper.url } : {}),
1564
- ...(typeof paper.score === "number" && Number.isFinite(paper.score) ? { score: paper.score } : {}),
1565
- ...(paper.reason ? { reason: paper.reason } : {}),
1566
- fullTextRead: false,
1567
- readStatus: "metadata",
1568
- unreadReason: "Full text not fetched yet; pending strict full-text bootstrap.",
1569
- }));
1570
- if (strictCoreSeed.length > 0) {
1571
- const strictAttemptLimit = Math.max(1, Math.min(MAX_STRICT_FULLTEXT_ATTEMPTS, effectiveRunLog?.requiredCorePapers ?? strictCoreSeed.length));
1572
- const backfilled = await backfillStrictCoreFullText({
1573
- corePapers: strictCoreSeed,
1574
- maxAttempts: strictAttemptLimit,
1575
- });
1576
- const strictRunLog = {
1577
- ...(effectiveRunLog ?? { runProfile: "strict" }),
1578
- fullTextAttempted: backfilled.attempted,
1579
- fullTextCompleted: backfilled.completed,
1580
- notes: [
1581
- effectiveRunLog?.notes,
1582
- `strict_fulltext_bootstrap attempted=${backfilled.attempted} completed=${backfilled.completed}`,
1583
- ...(backfilled.failures.length > 0
1584
- ? [`strict_fulltext_failures=${backfilled.failures.slice(0, 8).join(" | ")}`]
1585
- : []),
1586
- ]
1587
- .filter((item) => Boolean(item && item.trim().length > 0))
1588
- .join(" || "),
1589
- };
1590
- effectiveRunLog = strictRunLog;
1591
- effectiveKnowledgeState = {
1592
- ...(effectiveKnowledgeState ?? {}),
1593
- corePapers: backfilled.corePapers,
1594
- runLog: strictRunLog,
1595
- };
1596
- }
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
- }
1666
- const statusRaw = normalizeText(args.status ?? "").toLowerCase();
1667
- const researchArtifactsCount = effectivePapers.length +
1668
- (effectiveKnowledgeState?.explorationPapers?.length ?? 0) +
1669
- (effectiveKnowledgeState?.knowledgeChanges?.length ?? 0) +
1670
- (effectiveKnowledgeState?.knowledgeUpdates?.length ?? 0) +
1671
- (effectiveKnowledgeState?.hypotheses?.length ?? 0) +
1672
- (effectiveKnowledgeState?.explorationTrace?.length ?? 0);
1673
- let normalizedStatus = statusRaw.length > 0 ? statusRaw : undefined;
1674
- const coercedFromEmptyWithArtifacts = normalizedStatus === "empty" && researchArtifactsCount > 0;
1675
- if (coercedFromEmptyWithArtifacts) {
1676
- normalizedStatus = "degraded_quality";
1677
- }
1678
- const hasRunError = Boolean(effectiveKnowledgeState?.runLog?.error && normalizeText(effectiveKnowledgeState.runLog.error).length > 0);
1679
- const requiresArtifacts = normalizedStatus === "ok" || normalizedStatus === "fallback_representative" || normalizedStatus === "degraded_quality";
1680
- if (requiresArtifacts && researchArtifactsCount === 0 && !hasRunError) {
1681
- throw new Error("record payload has no research artifacts. Use status=empty for no-result runs, or include run_log.error for failed runs.");
1682
- }
1683
505
  let recordedPapers = 0;
1684
- for (const rawPaper of effectivePapers) {
506
+ for (const rawPaper of args.papers) {
1685
507
  const id = derivePaperId(rawPaper);
1686
508
  const existing = topicState.pushedPapers[id];
1687
509
  if (existing) {
@@ -1716,12 +538,7 @@ export async function recordIncrementalPush(args) {
1716
538
  }
1717
539
  topicState.totalRuns += 1;
1718
540
  topicState.lastRunAtMs = now;
1719
- topicState.lastStatus = normalizedStatus ?? (recordedPapers > 0 ? "ok" : "empty");
1720
- const effectiveNote = coercedFromEmptyWithArtifacts
1721
- ? [args.note?.trim(), "status coerced: empty -> degraded_quality because research artifacts were present"]
1722
- .filter((item) => Boolean(item && item.length > 0))
1723
- .join(" | ")
1724
- : args.note;
541
+ topicState.lastStatus = args.status?.trim() || (recordedPapers > 0 ? "ok" : "empty");
1725
542
  const knowledgeCommitted = await commitKnowledgeRun({
1726
543
  projectId: args.projectId ?? topicState.lastProjectId,
1727
544
  scope: topicState.scope,
@@ -1729,9 +546,9 @@ export async function recordIncrementalPush(args) {
1729
546
  topicKey: topicState.topicKey,
1730
547
  status: topicState.lastStatus,
1731
548
  runId: args.runId,
1732
- note: effectiveNote,
1733
- papers: effectivePapers,
1734
- knowledgeState: effectiveKnowledgeState,
549
+ note: args.note,
550
+ papers: args.papers,
551
+ knowledgeState: args.knowledgeState,
1735
552
  });
1736
553
  topicState.lastStatus = knowledgeCommitted.summary.lastStatus ?? topicState.lastStatus;
1737
554
  topicState.lastProjectId = knowledgeCommitted.projectId;
@@ -1743,20 +560,18 @@ export async function recordIncrementalPush(args) {
1743
560
  topicKey: topicState.topicKey,
1744
561
  status: topicState.lastStatus,
1745
562
  runId: knowledgeCommitted.runId,
1746
- run_id: knowledgeCommitted.runId,
1747
- run_profile: effectiveKnowledgeState?.runLog?.runProfile ?? null,
1748
563
  projectId: knowledgeCommitted.projectId,
1749
564
  streamKey: knowledgeCommitted.streamKey,
1750
565
  preferences: topicState.preferences,
1751
566
  recordedPapers,
1752
- papers: effectivePapers.map((paper) => ({
567
+ papers: args.papers.map((paper) => ({
1753
568
  id: derivePaperId(paper),
1754
569
  title: paper.title?.trim(),
1755
570
  url: paper.url?.trim(),
1756
571
  ...(typeof paper.score === "number" && Number.isFinite(paper.score) ? { score: paper.score } : {}),
1757
572
  ...(paper.reason ? { reason: paper.reason.trim() } : {}),
1758
573
  })),
1759
- note: effectiveNote,
574
+ note: args.note,
1760
575
  knowledgeStateSummary: knowledgeCommitted.summary,
1761
576
  });
1762
577
  return {
@@ -1765,7 +580,6 @@ export async function recordIncrementalPush(args) {
1765
580
  topicKey: topicState.topicKey,
1766
581
  preferences: topicState.preferences,
1767
582
  memoryHints: buildMemoryHints(memory),
1768
- runId: knowledgeCommitted.runId,
1769
583
  recordedPapers,
1770
584
  totalKnownPapers: Object.keys(topicState.pushedPapers).length,
1771
585
  pushedAtMs: now,
@@ -1861,11 +675,6 @@ export async function getIncrementalStateStatus(args) {
1861
675
  const lastPushedAtMs = excludePaperIds.length
1862
676
  ? topicState.pushedPapers[excludePaperIds[0]]?.lastPushedAtMs
1863
677
  : undefined;
1864
- const knowledgeStateMissingReason = knowledgeSummaryResult === undefined
1865
- ? args.projectId || topicState.lastProjectId
1866
- ? "project_or_stream_not_found"
1867
- : "project_unbound"
1868
- : undefined;
1869
678
  return {
1870
679
  scope: topicState.scope,
1871
680
  topic: topicState.topic,
@@ -1879,7 +688,6 @@ export async function getIncrementalStateStatus(args) {
1879
688
  ...(topicState.lastStatus ? { lastStatus: topicState.lastStatus } : {}),
1880
689
  recentPapers: recentPapersByRecency(topicState.pushedPapers, 10),
1881
690
  ...(knowledgeSummaryResult ? { knowledgeStateSummary: knowledgeSummaryResult.summary } : {}),
1882
- ...(knowledgeStateMissingReason ? { knowledgeStateMissingReason } : {}),
1883
691
  recentHypotheses: knowledgeSummaryResult?.summary.recentHypotheses ?? [],
1884
692
  recentChangeStats: knowledgeSummaryResult?.summary.recentChangeStats ?? [],
1885
693
  lastExplorationTrace: knowledgeSummaryResult?.summary.lastExplorationTrace ?? [],