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.
- package/README.en.md +350 -0
- package/README.md +148 -358
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +131 -122
- package/dist/index.js.map +1 -1
- package/dist/src/cli/research.d.ts +1 -6
- package/dist/src/cli/research.d.ts.map +1 -1
- package/dist/src/cli/research.js +227 -123
- package/dist/src/cli/research.js.map +1 -1
- package/dist/src/commands/metabolism-status.d.ts +3 -3
- package/dist/src/commands/metabolism-status.d.ts.map +1 -1
- package/dist/src/commands/metabolism-status.js +72 -75
- package/dist/src/commands/metabolism-status.js.map +1 -1
- package/dist/src/commands.d.ts +1 -1
- package/dist/src/commands.d.ts.map +1 -1
- package/dist/src/commands.js +0 -55
- package/dist/src/commands.js.map +1 -1
- package/dist/src/hooks/cron-skill-inject.d.ts +6 -7
- package/dist/src/hooks/cron-skill-inject.d.ts.map +1 -1
- package/dist/src/hooks/cron-skill-inject.js +6 -15
- package/dist/src/hooks/cron-skill-inject.js.map +1 -1
- package/dist/src/hooks/research-mode.d.ts +1 -1
- package/dist/src/hooks/research-mode.d.ts.map +1 -1
- package/dist/src/hooks/research-mode.js +24 -72
- package/dist/src/hooks/research-mode.js.map +1 -1
- package/dist/src/hooks/scientify-signature.d.ts +1 -1
- package/dist/src/hooks/scientify-signature.d.ts.map +1 -1
- package/dist/src/hooks/scientify-signature.js +2 -5
- package/dist/src/hooks/scientify-signature.js.map +1 -1
- package/dist/src/knowledge-state/render.d.ts +1 -9
- package/dist/src/knowledge-state/render.d.ts.map +1 -1
- package/dist/src/knowledge-state/render.js +33 -158
- package/dist/src/knowledge-state/render.js.map +1 -1
- package/dist/src/knowledge-state/store.d.ts.map +1 -1
- package/dist/src/knowledge-state/store.js +65 -884
- package/dist/src/knowledge-state/store.js.map +1 -1
- package/dist/src/knowledge-state/types.d.ts +0 -69
- package/dist/src/knowledge-state/types.d.ts.map +1 -1
- package/dist/src/literature/subscription-state.d.ts +0 -2
- package/dist/src/literature/subscription-state.d.ts.map +1 -1
- package/dist/src/literature/subscription-state.js +7 -1199
- package/dist/src/literature/subscription-state.js.map +1 -1
- package/dist/src/research-subscriptions/constants.d.ts +1 -1
- package/dist/src/research-subscriptions/constants.js +1 -1
- package/dist/src/research-subscriptions/cron-client.d.ts +1 -1
- package/dist/src/research-subscriptions/cron-client.d.ts.map +1 -1
- package/dist/src/research-subscriptions/delivery.d.ts +1 -1
- package/dist/src/research-subscriptions/delivery.d.ts.map +1 -1
- package/dist/src/research-subscriptions/handlers.d.ts +1 -1
- package/dist/src/research-subscriptions/handlers.d.ts.map +1 -1
- package/dist/src/research-subscriptions/handlers.js +9 -9
- package/dist/src/research-subscriptions/handlers.js.map +1 -1
- package/dist/src/research-subscriptions/parse.d.ts.map +1 -1
- package/dist/src/research-subscriptions/parse.js +0 -10
- package/dist/src/research-subscriptions/parse.js.map +1 -1
- package/dist/src/research-subscriptions/prompt.d.ts +1 -1
- package/dist/src/research-subscriptions/prompt.d.ts.map +1 -1
- package/dist/src/research-subscriptions/prompt.js +196 -191
- package/dist/src/research-subscriptions/prompt.js.map +1 -1
- package/dist/src/research-subscriptions/types.d.ts +1 -2
- package/dist/src/research-subscriptions/types.d.ts.map +1 -1
- package/dist/src/templates/bootstrap.d.ts.map +1 -1
- package/dist/src/templates/bootstrap.js +32 -19
- package/dist/src/templates/bootstrap.js.map +1 -1
- package/dist/src/tools/arxiv-download.d.ts +1 -2
- package/dist/src/tools/arxiv-download.d.ts.map +1 -1
- package/dist/src/tools/arxiv-search.d.ts +1 -2
- package/dist/src/tools/arxiv-search.d.ts.map +1 -1
- package/dist/src/tools/github-search-tool.d.ts +1 -2
- package/dist/src/tools/github-search-tool.d.ts.map +1 -1
- package/dist/src/tools/openalex-search.d.ts +1 -2
- package/dist/src/tools/openalex-search.d.ts.map +1 -1
- package/dist/src/tools/openreview-lookup.d.ts +1 -2
- package/dist/src/tools/openreview-lookup.d.ts.map +1 -1
- package/dist/src/tools/paper-browser.d.ts +1 -2
- package/dist/src/tools/paper-browser.d.ts.map +1 -1
- package/dist/src/tools/result.d.ts +3 -5
- package/dist/src/tools/result.d.ts.map +1 -1
- package/dist/src/tools/result.js +5 -7
- package/dist/src/tools/result.js.map +1 -1
- package/dist/src/tools/scientify-cron.d.ts +4 -9
- package/dist/src/tools/scientify-cron.d.ts.map +1 -1
- package/dist/src/tools/scientify-cron.js +19 -441
- package/dist/src/tools/scientify-cron.js.map +1 -1
- package/dist/src/tools/scientify-literature-state.d.ts +1 -62
- package/dist/src/tools/scientify-literature-state.d.ts.map +1 -1
- package/dist/src/tools/scientify-literature-state.js +46 -312
- package/dist/src/tools/scientify-literature-state.js.map +1 -1
- package/dist/src/tools/unpaywall-download.d.ts +1 -2
- package/dist/src/tools/unpaywall-download.d.ts.map +1 -1
- package/dist/src/types.d.ts +16 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/openclaw.plugin.json +4 -2
- package/package.json +1 -1
- package/skills/metabolism/SKILL.md +2 -0
- package/skills/research-subscription/SKILL.md +1 -29
- 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(/ /gi, " ")
|
|
244
|
-
.replace(/&/gi, "&")
|
|
245
|
-
.replace(/</gi, "<")
|
|
246
|
-
.replace(/>/gi, ">")
|
|
247
|
-
.replace(/"/gi, "\"")
|
|
248
|
-
.replace(/'/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(/</g, "<")
|
|
494
|
-
.replace(/>/g, ">")
|
|
495
|
-
.replace(/&/g, "&")
|
|
496
|
-
.replace(/"/g, "\"")
|
|
497
|
-
.replace(/'/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
|
|
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 =
|
|
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:
|
|
1733
|
-
papers:
|
|
1734
|
-
knowledgeState:
|
|
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:
|
|
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:
|
|
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 ?? [],
|